diff --git a/conformance/byteslengen/main.go b/conformance/byteslengen/main.go new file mode 100644 index 00000000..b8aac76b --- /dev/null +++ b/conformance/byteslengen/main.go @@ -0,0 +1,62 @@ +// Command byteslengen emits bytes_max_len.json: per message, the JSON field names of +// every bytes field declaring (buf.validate.field).bytes.max_len, with the byte limit. +// +// It exists because protoschema-plugins renders that rule as a maxLength on the base64 +// string that counts the padding characters, which is not exact: 32768 bytes and 32769 +// bytes both encode to 43692 characters, so a token one byte over the limit passes the +// JSON Schema while Go protovalidate rejects it. merge_schema.py and gen_jsonschema.py +// read this manifest and rewrite the field's pattern to allow at most ceil(4n/3) data +// characters, which is exact, so the generated Pydantic/Zod models, the published +// schemas and the strict validators refuse exactly what the Go server refuses. +// +// Like requiredgen and uniquegen, this is a Go program because Go protovalidate is the +// authoritative view of the rules. Scope comes from conformance.Contract. +package main + +import ( + "encoding/json" + "os" + + protovalidate "buf.build/go/protovalidate" + "google.golang.org/protobuf/reflect/protoreflect" + + "github.com/FORA-Protocol/protocol/conformance" +) + +func main() { + out := "bytes_max_len.json" + if len(os.Args) > 1 { + out = os.Args[1] + } + limits := map[string]map[string]uint64{} + conformance.EachMessage(func(md protoreflect.MessageDescriptor) { + for i := 0; i < md.Fields().Len(); i++ { + fd := md.Fields().Get(i) + if n := bytesMaxLen(fd); n > 0 { + if limits[string(md.Name())] == nil { + limits[string(md.Name())] = map[string]uint64{} + } + limits[string(md.Name())][string(fd.Name())] = n + } + } + }) + b, err := json.MarshalIndent(limits, "", " ") + if err != nil { + panic(err) + } + if err := os.WriteFile(out, append(b, '\n'), 0o644); err != nil { + panic(err) + } +} + +// bytesMaxLen returns the bytes.max_len rule of a singular bytes field, or 0. +func bytesMaxLen(fd protoreflect.FieldDescriptor) uint64 { + if fd.Kind() != protoreflect.BytesKind || fd.IsList() { + return 0 + } + fr, err := protovalidate.ResolveFieldRules(fd) + if err != nil || fr == nil { + return 0 + } + return fr.GetBytes().GetMaxLen() +} diff --git a/conformance/corpus/cases.json b/conformance/corpus/cases.json index f56ef980..01eac70a 100644 --- a/conformance/corpus/cases.json +++ b/conformance/corpus/cases.json @@ -1831,6 +1831,38 @@ "unit_cost": "0" } }, + { + "id": "Delegation/token/max_ok", + "message": "Delegation", + "valid": true, + "json": { + "expires_at": "2026-01-02T03:04:05Z", + "quota_period": "90s", + "token": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE=" + } + }, + { + "id": "Delegation/token/too_long", + "message": "Delegation", + "valid": false, + "rules": [ + "bytes.max_len" + ], + "json": { + "expires_at": "2026-01-02T03:04:05Z", + "quota_period": "90s", + "token": "YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFh" + } + }, + { + "id": "Delegation/valid", + "message": "Delegation", + "valid": true, + "json": { + "expires_at": "2026-01-02T03:04:05Z", + "quota_period": "90s" + } + }, { "id": "DiscoveryRequest/requester/missing", "message": "DiscoveryRequest", diff --git a/conformance/corpusgen/main.go b/conformance/corpusgen/main.go index 334445fb..2f58d2a1 100644 --- a/conformance/corpusgen/main.go +++ b/conformance/corpusgen/main.go @@ -15,6 +15,7 @@ package main import ( + "bytes" "encoding/json" "fmt" "os" @@ -681,6 +682,8 @@ func validScalar(fd protoreflect.FieldDescriptor, fr *validate.FieldRules) (prot } } return protoreflect.ValueOfFloat64(0), nil + case protoreflect.BytesKind: + return protoreflect.ValueOfBytes(nil), nil // the contract's bytes rules only bound length from above case protoreflect.MessageKind: return protoreflect.Value{}, nil // handled by caller via seed } @@ -709,6 +712,8 @@ func edges(fd protoreflect.FieldDescriptor, fr *validate.FieldRules, sd map[stri es = append(es, enumEdges(fd, fr.GetEnum())...) case protoreflect.StringKind: es = append(es, stringEdges(fd, fr.GetString())...) + case protoreflect.BytesKind: + es = append(es, bytesEdges(fd, fr.GetBytes())...) case protoreflect.Int64Kind: if r := fr.GetInt64(); r != nil { if _, ok := r.GetGreaterThan().(*validate.Int64Rules_Gte); ok { @@ -877,6 +882,23 @@ func stringEdges(fd protoreflect.FieldDescriptor, r *validate.StringRules) []edg return es } +// bytesEdges emits the length boundary of a bytes field: exactly max_len bytes is a +// positive edge Go must accept, max_len+1 bytes trips bytes.max_len. The bytes are a +// run of one letter, deliberately not a parseable token, so the corpus proves the wire +// size rule alone and never a format rule. +func bytesEdges(fd protoreflect.FieldDescriptor, r *validate.BytesRules) []edge { + var es []edge + if n := r.GetMaxLen(); n > 0 { + fit := bytes.Repeat([]byte{'a'}, int(n)) + over := bytes.Repeat([]byte{'a'}, int(n)+1) + es = append(es, edge{label: "max_ok", valid: true, + apply: func(m protoreflect.Message) { m.Set(fd, protoreflect.ValueOfBytes(fit)) }}) + es = append(es, edge{label: "too_long", want: "bytes.max_len", + apply: func(m protoreflect.Message) { m.Set(fd, protoreflect.ValueOfBytes(over)) }}) + } + return es +} + // failingBadStringIdxs returns the indices of badStrings the pattern rejects, in order. // Shared by stringEdges (multi-emit) and listEdges item_pattern (first only), so the two // call sites stay in lockstep on what "a bad string for this pattern" means. @@ -975,6 +997,9 @@ func hasConstraint(fr *validate.FieldRules) bool { if s := fr.GetString(); s != nil && (s.GetPattern() != "" || s.GetMinLen() > 0 || s.GetMaxLen() > 0) { return true } + if b := fr.GetBytes(); b != nil && b.GetMaxLen() > 0 { + return true + } if i := fr.GetInt64(); i != nil && i.GetGreaterThan() != nil { return true } diff --git a/conformance/doc_claim_guard_test.go b/conformance/doc_claim_guard_test.go new file mode 100644 index 00000000..6ed30a4c --- /dev/null +++ b/conformance/doc_claim_guard_test.go @@ -0,0 +1,52 @@ +package conformance + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +func TestDocDelegationClaimGuard(t *testing.T) { + script, err := os.ReadFile("../scripts/check-doc-conformance.sh") + if err != nil { + t.Fatal(err) + } + for _, tc := range []struct { + name, claims, refusal string + }{ + {"jwt_only", "fora_parent fora_advisory", ""}, + {"mirror", "fora_max_spend_cents", ""}, + {"unknown", "fora_parnet", "registered delegation claim 'fora_parnet' has no matching"}, + {"other_message_field", "fora_offer_id", "registered delegation claim 'fora_offer_id' has no matching"}, + } { + t.Run(tc.name, func(t *testing.T) { + root := t.TempDir() + for path, content := range map[string]string{ + "scripts/check-doc-conformance.sh": string(script), + "proto/fora/v1/fora.proto": "message Delegation {\n int64 max_spend_cents = 1;\n}\n" + + "message TransactionItem {\n string offer_id = 1;\n}\n", + "website/src/content/docs/protocol/authentication.mdx": tc.claims, + "website/src/content/docs/components/transaction-log/event-types.mdx": "", + "docs/placeholder.md": "", + } { + target := filepath.Join(root, path) + if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(target, []byte(content), 0644); err != nil { + t.Fatal(err) + } + } + out, err := exec.Command("bash", filepath.Join(root, "scripts/check-doc-conformance.sh")).CombinedOutput() + if tc.refusal == "" { + if err != nil { + t.Fatalf("valid claim rejected: %v\n%s", err, out) + } + } else if err == nil || !strings.Contains(string(out), tc.refusal) { + t.Fatalf("expected refusal %q, got %v\n%s", tc.refusal, err, out) + } + }) + } +} diff --git a/conformance/docdelegation_test.go b/conformance/docdelegation_test.go new file mode 100644 index 00000000..09b72e24 --- /dev/null +++ b/conformance/docdelegation_test.go @@ -0,0 +1,153 @@ +package conformance + +import ( + "crypto/ed25519" + "encoding/base64" + "encoding/json" + "os" + "path/filepath" + "regexp" + "strings" + "testing" + "time" + + "github.com/FORA-Protocol/protocol/sdk/go/helpers" +) + +// Documented delegation tokens must verify. +// +// A page that shows a delegation token shows a real one, taken from the shared +// chain cases (sdk/go/helpers/testdata/delegation-chains.json), and names the +// case on the line above the fence: {/* delegation-chain: */}. The +// fenced block is either a proto-JSON request whose requester.delegation.token +// carries the token base64-encoded, or the bare compact JWS. This test finds +// every such block, checks the token is byte-identical to the named case, and +// verifies it with helpers.VerifyDelegation under the case's trust input, +// audience, holder thumbprint and clock, expecting the case's declared outcome. +// A proto-JSON block that carries a real-looking token without a marker fails +// too, so no page can show a token nothing verifies. + +var chainMarkedFenceRe = regexp.MustCompile("(?s)(?:\\{/\\* delegation-chain: ([a-z0-9_]+) \\*/\\}\\s*\\n)?(?:\\{/\\* fora-validate: [A-Za-z0-9]+ \\*/\\}\\s*\\n)?```[a-zA-Z0-9]*[ \t]*\\n(.*?)```") + +type docChainCase struct { + Name string `json:"name"` + Token string `json:"token"` + Trusted []struct { + Identity string `json:"identity"` + KeyX string `json:"key_x"` + } `json:"trusted"` + Audience string `json:"audience"` + Holder string `json:"holder"` + Now string `json:"now"` + Expect struct { + Valid bool `json:"valid"` + Scopes []string `json:"scopes"` + ExpiresAt string `json:"expires_at"` + } `json:"expect"` +} + +func loadDocChainCases(t *testing.T) map[string]docChainCase { + t.Helper() + raw, err := os.ReadFile(filepath.Join("..", "sdk", "go", "helpers", "testdata", "delegation-chains.json")) + if err != nil { + t.Fatal(err) + } + var doc struct { + Cases []docChainCase `json:"cases"` + } + if err := json.Unmarshal(raw, &doc); err != nil { + t.Fatal(err) + } + out := map[string]docChainCase{} + for _, c := range doc.Cases { + out[c.Name] = c + } + return out +} + +// documentedToken returns the token a fenced block documents: the base64 +// (standard or URL, padded or not) requester.delegation.token of a proto-JSON +// request, or the block itself when it is one bare compact JWS. +func documentedToken(body string) (token string, found bool) { + trimmed := strings.TrimSpace(body) + if strings.Count(trimmed, ".") == 2 && !strings.ContainsAny(trimmed, " \n{}") { + return trimmed, true + } + var req struct { + Requester struct { + Delegation struct { + Token string `json:"token"` + } `json:"delegation"` + } `json:"requester"` + } + if err := json.Unmarshal([]byte(trimmed), &req); err != nil || req.Requester.Delegation.Token == "" { + return "", false + } + enc := req.Requester.Delegation.Token + for _, e := range []*base64.Encoding{base64.StdEncoding, base64.RawStdEncoding, base64.URLEncoding, base64.RawURLEncoding} { + if b, err := e.DecodeString(enc); err == nil { + return string(b), true + } + } + return enc, true +} + +func TestDocDelegationTokensVerify(t *testing.T) { + cases := loadDocChainCases(t) + verified := 0 + walkDocs(t, func(path, content string) { + for _, m := range chainMarkedFenceRe.FindAllStringSubmatch(content, -1) { + name, body := m[1], m[2] + token, found := documentedToken(body) + if !found { + continue + } + if name == "" { + if isPlaceholder(token) { + continue + } + t.Errorf("%s: a documented delegation token has no {/* delegation-chain: */} marker, so nothing verifies it", filepath.Base(path)) + continue + } + c, ok := cases[name] + if !ok { + t.Errorf("%s: delegation-chain %q is not a case in delegation-chains.json", filepath.Base(path), name) + continue + } + if token != c.Token { + t.Errorf("%s: the documented token differs from case %q", filepath.Base(path), name) + continue + } + in := helpers.DelegationInput{Token: []byte(token), Audience: c.Audience, Holder: c.Holder} + var err error + if in.Now, err = time.Parse(time.RFC3339, c.Now); err != nil { + t.Fatal(err) + } + for _, tr := range c.Trusted { + x, err := base64.RawURLEncoding.DecodeString(tr.KeyX) + if err != nil { + t.Fatal(err) + } + in.TrustedIssuers = append(in.TrustedIssuers, helpers.TrustedIssuer{Identity: tr.Identity, Key: ed25519.PublicKey(x)}) + } + got, err := helpers.VerifyDelegation(in) + if !c.Expect.Valid { + t.Errorf("%s: case %q is a refusal case; a page documents only chains that verify", filepath.Base(path), name) + continue + } + if err != nil { + t.Errorf("%s: documented token %q does not verify: %v", filepath.Base(path), name, err) + continue + } + if strings.Join(got.Scopes, " ") != strings.Join(c.Expect.Scopes, " ") || got.ExpiresAt.UTC().Format(time.RFC3339) != c.Expect.ExpiresAt { + t.Errorf("%s: documented token %q verified to %+v, case declares %+v", filepath.Base(path), name, got, c.Expect) + continue + } + verified++ + } + }) + if verified == 0 { + t.Fatal("no documented delegation token was verified; the docs must show at least one real chain") + } + t.Logf("documented delegation tokens verified=%d", verified) +} diff --git a/conformance/docgo/requestflows/handler.go b/conformance/docgo/requestflows/handler.go new file mode 100644 index 00000000..4f3d1858 --- /dev/null +++ b/conformance/docgo/requestflows/handler.go @@ -0,0 +1,259 @@ +// Code generated from website/src/content/docs/components/exchange/request-flows.mdx +// ("Handler Implementation"). DO NOT EDIT. Regenerate with +// FORA_UPDATE_VECTORS=1 go test ./conformance -run TestDocHandlerExampleCompiles + +package requestflows + +import ( + "context" + "errors" + "time" + + "connectrpc.com/connect" + "google.golang.org/protobuf/proto" + + forav1 "github.com/FORA-Protocol/protocol/gen/go/fora/v1" + "github.com/FORA-Protocol/protocol/sdk/go/connectserver" + "github.com/FORA-Protocol/protocol/sdk/go/helpers" +) + +func (h *ExchangeHandler) DiscoverResources( + ctx context.Context, + req *connect.Request[forav1.ResourceQuery], +) (*connect.Response[forav1.ResourceResponse], error) { + // 1. Validate + if err := validate(req.Msg); err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, err) + } + + // 1b. Respect deadline field from ResourceQuery. + // If req.Msg.Deadline is set, use it as the context timeout. + // If absent, use a 500ms default timeout for the entire handler. + if req.Msg.Deadline != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, req.Msg.Deadline.AsDuration()) + defer cancel() + } else { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, 500*time.Millisecond) + defer cancel() + } + + // 2. Requester identity. Verification already happened at the HTTP seam: + // the SDK's verify middleware checked the RFC 9421 signature of the + // party that sent the request — the agent, or a Broker that originated + // or re-packaged it (Authentication > Multi-hop forwarding) — resolving the signer's key + // by its signed keyid (an RFC 7638 thumbprint) in the key directory + // named by the `Signature-Agent` member that signature covers. The + // self-asserted Requester.domain field never drives request-signing key + // lookup. + // The handler consumes the proof from the context; it never re-fetches + // keys or re-verifies bytes, and it fails closed when the proof is + // absent (a route the middleware does not guard). + requester := req.Msg.Requester + verified := helpers.FromContext(ctx) // the first verified signature in header order; all of them via helpers.AllSignaturesFromContext + if verified == nil { + return nil, connect.NewError(connect.CodeUnauthenticated, + errors.New("request reached the handler without a verified signature")) + } + + // 2b. Direct or Broker-led ("Direct and relayed requests" in the protocol). + // Requester.domain is the host of the agent's key directory, never a + // free label. When it names the directory the signature resolved from + // (the host of the covered Signature-Agent URL, compared by the + // request-recipient identity rule: case-folded, an absent port the same + // as :443), the query is DIRECT: the agent signed it. When it names + // another directory, the query is one a Broker originated, which this + // Exchange acts on only when it admits the signer as a Broker; there the + // domain is the Broker's statement of whom it queries for, and nothing + // on this leg authenticates the agent. Any other mismatch is a signature + // that verifies, but not for the requester the body names. + host, err := helpers.HostOf(verified.SignatureAgent) + verdict := helpers.AudienceMalformed + if err == nil { + verdict, err = helpers.CheckAudience(host, requester.GetDomain()) + } + direct := err == nil && verdict == helpers.AudienceAccepted + brokerLed := err == nil && verdict == helpers.AudienceMismatch && + h.brokers.Admits(verified.SignatureAgent) + if !direct && !brokerLed { + return nil, connectserver.AttachDetail( + connect.NewError(connect.CodeUnauthenticated, errors.New("requester.domain is not the signing directory")), + helpers.RequestAuthFailureDetail("fora.v1.ExchangeService", "requester.domain is not the signing directory", + forav1.RequestAuthFailureReason_REQUEST_AUTH_FAILURE_REASON_SIGNATURE_INVALID)) + } + + // 2c. On a DIRECT query, resolve the agent's registered account from the + // directory the signature resolved from (verified.SignatureAgent; see + // "Accounts" on the authentication page). Every currently valid key that + // directory publishes acts for the account, so the key that verified + // does not select it, and neither does requester.id, a free label. A + // query a Broker originated authenticates no agent, so this Exchange + // consults no account for it (its own choice; the protocol leaves it + // open) and presents on the stated scopes alone. + var account *Account + if direct { + account, err = h.accounts.ForDirectory(ctx, verified.SignatureAgent) + if err != nil { + return nil, connect.NewError(connect.CodePermissionDenied, err) + } + } + + // 3. Resolve tenant + tenantCfg, err := h.tenants.ResolveFromURIs(req.Msg.Uris) + if err != nil { + return nil, connect.NewError(connect.CodeNotFound, err) + } + + // 3b. Verify the delegation (if present) on a DIRECT query, once per + // subscription record. Trust is per record: the SDK verifier is given + // the keys that record lists, so an issuer trusted for one subscription + // grants nothing on another. A record whose keys refuse the chain keeps + // its gated terms hidden; the refusal is journaled, never reported. The + // holder is the RFC 9421 signer's key from the HTTP proof of step 2, + // which stays separate from the delegation result. On a query a Broker + // originated the Broker signs alone, so nothing can pass the holder + // binding: the delegation is not honoured, and the query is answered + // as if none were present. A token over the size limit never gets + // here: step 1 refused the request. + grants := map[string]*helpers.VerifiedDelegation{} // subscription record ID -> verified grant + if direct && requester.GetDelegation() != nil { + holder, err := helpers.Thumbprint(verified.PublicKey) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, err) + } + for _, record := range h.subscriptions.RecordsForTenant(ctx, tenantCfg.ID) { + grant, err := helpers.VerifyDelegation(helpers.DelegationInput{ + Token: requester.GetDelegation().GetToken(), + TrustedIssuers: record.TrustedIssuers, + Audience: tenantCfg.Identity, + Holder: holder, + Now: h.clock.Now(), + }) + if err != nil { + h.journal.DelegationRefused(ctx, record.ID, err) + continue + } + grants[record.ID] = grant + } + } + + // 4. Application-level abuse check (optional — volumetric rate limiting is at infra layer) + // The Exchange MAY return 429 for application-level abuse, e.g., query-to-transaction + // ratio too low (agent discovering but never buying). Volumetric rate limiting (per-IP, + // per-tenant RPS caps) belongs in HAProxy/Envoy/API Gateway in front of this service. + + // 5. Catalog lookup (lock-free read, scope-filtered) + // A non-gated term is filtered by requester.scopes, the requester's own + // statement — terms outside the stated scopes are not returned. A gated + // term (a subscription term) is presented only when the grant verified + // under the term's own subscription record covers the term's scopes, by + // the one matching rule on Requester.scopes; requester.scopes is ignored + // for it, and a record with no grant — every record on a query a Broker + // originated (3b) — keeps its gated terms hidden, as if they did not + // exist. The catalog bounds each gated offer's expires_at by its grant's + // ExpiresAt, so the offer never outlives the chain. + offers, err := h.catalog.FindOffers(ctx, tenantCfg.ID, req.Msg.Uris, requester.Scopes, grants) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, err) + } + + // 5b. Check for active subscription, on a DIRECT query (step 2d). + // If the requester has an active subscription with this tenant, include a + // subscription offer variant (rate=0) for each catalog offer with + // SubscriptionQuotaInfo attached. Quota enforcement is deferred to + // ExecuteTransaction time — DiscoverResources checks subscription status + // and populates quota visibility. Subscription/entitlement is resolved + // from the registered account of step 2d (plus any delegation) — never + // from the caller-written requester.id. A query a Broker originated has + // no account here, so it gets no subscription variant. + var subStatus *SubscriptionStatus + if account != nil { + if sub, err := h.billing.CheckSubscription(ctx, account.BillingRef, tenantCfg.ID); err == nil && sub.Active { + subStatus = sub + } + } + + // Build subscription offer variants per catalog offer (if subscription active). + // Each subscription offer includes SubscriptionQuotaInfo so agents can see + // remaining quota before committing to a transaction. + if subStatus != nil { + var subOffers []*forav1.Offer + for _, offer := range offers { + subOffer := proto.Clone(offer).(*forav1.Offer) + // A subscription variant is a distinct offer, so it gets a fresh + // opaque offer_id — never one derived from the base offer's id. + subOffer.OfferId = "offer-" + newOpaqueID() + subOffer.SubscriptionId = proto.String(subStatus.SubscriptionID) + subOffer.Pricing = &forav1.Pricing{ + Model: offer.Pricing.Model, + Rate: "0", // zero marginal cost under subscription; money is a decimal string + Currency: offer.Pricing.Currency, + UnitCost: proto.String("0"), + EstimatedQuantity: offer.Pricing.EstimatedQuantity, + } + // Populate SubscriptionQuotaInfo for proactive quota signaling + subOffer.SubscriptionQuota = h.billing.SubscriptionQuota( + ctx, subStatus.SubscriptionID, tenantCfg.ID, + ) + subOffers = append(subOffers, subOffer) + } + offers = append(offers, subOffers...) + } + + // 6. Attach reporting obligations (per-tenant default, not per-offer lookup) + // Reporting obligation is set once in tenant config. All offers inherit the + // tenant's reporting obligation unless explicitly overridden (rare). + tenantReporting := h.reporting.GetPolicy(tenantCfg.ID) + for _, offer := range offers { + offer.Reporting = tenantReporting + } + + // 7. Ed25519-sign every offer (MANDATORY — enables stateless ExecuteTransaction) + // Ed25519 is the default for offers because agents and Brokers must + // verify signatures without holding the signing key. The Exchange signs + // with its Ed25519 private key; anyone verifies with the public key. + for _, offer := range offers { + sig, err := h.offerSigner.Sign(offer) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, err) + } + offer.Signature = sig + offer.SignatureAlgorithm = "EdDSA" + } + + // 8. Build response — flat offers or OfferGroups depending on URI count. + // When the ResourceQuery contains multiple URIs, return OfferGroups + // (one per URI) so the caller knows which offers map to which resource. + // When a single URI is queried, use the flat offers field. + uris := req.Msg.Uris + if len(uris) > 1 { + // Batch mode: group offers by requested URI. + groups := make([]*forav1.OfferGroup, 0, len(uris)) + for _, uri := range uris { + var uriOffers []*forav1.Offer + for _, offer := range offers { + if offerMatchesURI(offer, uri) { + uriOffers = append(uriOffers, offer) + } + } + groups = append(groups, &forav1.OfferGroup{ + Uri: uri, + Offers: uriOffers, // empty slice = resource not available for this URI + }) + } + return connect.NewResponse(&forav1.ResourceResponse{ + Ver: helpers.ProtocolVersion, + Exchange: h.exchangeDomain, + OfferGroups: groups, + }), nil + // Correlation is not echoed in the body: it rides the X-Request-ID + // HTTP header, propagated across the request/response round-trip. + } + + return connect.NewResponse(&forav1.ResourceResponse{ + Ver: helpers.ProtocolVersion, + Exchange: h.exchangeDomain, + Offers: offers, + }), nil +} diff --git a/conformance/docgo/requestflows/scaffold.go b/conformance/docgo/requestflows/scaffold.go new file mode 100644 index 00000000..14d04877 --- /dev/null +++ b/conformance/docgo/requestflows/scaffold.go @@ -0,0 +1,88 @@ +// Package requestflows compiles the DiscoverResources handler example of the +// Exchange request-flows page against typed stubs, so a variable used before +// it is declared, a name that is never defined or a wrong SDK call fails +// `go build` instead of sitting in the documentation. handler.go is extracted +// from the page by conformance/docgo_test.go; this file is the scaffold the +// example needs and the only hand-written part. +package requestflows + +import ( + "context" + "crypto/rand" + "encoding/hex" + "time" + + forav1 "github.com/FORA-Protocol/protocol/gen/go/fora/v1" + "github.com/FORA-Protocol/protocol/sdk/go/helpers" +) + +// ExchangeHandler is the handler the example is a method of. Every field is +// an interface the example calls; none has an implementation here. +type ExchangeHandler struct { + brokers interface { + Admits(signatureAgent string) bool + } + accounts interface { + ForDirectory(ctx context.Context, directory string) (*Account, error) + } + tenants interface { + ResolveFromURIs(uris []string) (*TenantConfig, error) + } + subscriptions interface { + RecordsForTenant(ctx context.Context, tenantID string) []SubscriptionRecord + } + journal interface { + DelegationRefused(ctx context.Context, recordID string, err error) + } + catalog interface { + FindOffers(ctx context.Context, tenantID string, uris, statedScopes []string, grants map[string]*helpers.VerifiedDelegation) ([]*forav1.Offer, error) + } + billing Billing + reporting interface { + GetPolicy(tenantID string) *forav1.ReportingObligation + } + offerSigner interface { + Sign(offer *forav1.Offer) (string, error) + } + clock interface{ Now() time.Time } + exchangeDomain string +} + +// Billing answers subscription questions for the account the query resolved. +type Billing interface { + CheckSubscription(ctx context.Context, billingRef, tenantID string) (*SubscriptionStatus, error) + SubscriptionQuota(ctx context.Context, subscriptionID, tenantID string) []*forav1.SubscriptionQuotaInfo +} + +// Account is the registered account a key directory resolves to. +type Account struct{ BillingRef string } + +// TenantConfig is the publisher tenant the queried URIs belong to. Identity is +// the Exchange's own recipient identity, the value a delegation's aud must name. +type TenantConfig struct{ ID, Identity string } + +// SubscriptionRecord is one subscription with the root issuer keys it trusts. +type SubscriptionRecord struct { + ID string + TrustedIssuers []helpers.TrustedIssuer +} + +// SubscriptionStatus is the billing view of one subscription. +type SubscriptionStatus struct { + Active bool + SubscriptionID string +} + +func validate(msg *forav1.ResourceQuery) error { return helpers.Validate(msg) } + +// offerMatchesURI reports whether an offer answers the queried URI. +func offerMatchesURI(offer *forav1.Offer, uri string) bool { + return offer.GetIdentity().GetCanonicalUrl() == uri +} + +// newOpaqueID mints an opaque offer id; the example only needs it to be fresh. +func newOpaqueID() string { + var b [16]byte + _, _ = rand.Read(b[:]) + return hex.EncodeToString(b[:]) +} diff --git a/conformance/docgo/requestflows/scaffold_test.go b/conformance/docgo/requestflows/scaffold_test.go new file mode 100644 index 00000000..8164659b --- /dev/null +++ b/conformance/docgo/requestflows/scaffold_test.go @@ -0,0 +1,7 @@ +package requestflows + +import "testing" + +// TestScaffoldCompiles exists so `go test ./...` builds this package; the +// compile itself is the check. +func TestScaffoldCompiles(t *testing.T) { _ = (&ExchangeHandler{}).DiscoverResources } diff --git a/conformance/docgo_test.go b/conformance/docgo_test.go new file mode 100644 index 00000000..6fc93140 --- /dev/null +++ b/conformance/docgo_test.go @@ -0,0 +1,55 @@ +package conformance + +import ( + "go/format" + "os" + "path/filepath" + "strings" + "testing" +) + +// TestDocHandlerExampleCompiles pins the DiscoverResources handler example of +// the Exchange request-flows page to conformance/docgo/requestflows/handler.go, +// a package that compiles the example against typed stubs. The page is the +// source: under FORA_UPDATE_VECTORS=1 the file is rewritten from it, otherwise +// the two must match, and `go test ./...` compiles the package, so an example +// that uses a variable before declaring it or calls the SDK wrongly fails CI. +func TestDocHandlerExampleCompiles(t *testing.T) { + page, err := os.ReadFile(filepath.Join("..", "website", "src", "content", "docs", "components", "exchange", "request-flows.mdx")) + if err != nil { + t.Fatal(err) + } + content := string(page) + i := strings.Index(content, "### Handler Implementation") + if i < 0 { + t.Fatal("request-flows.mdx has no Handler Implementation section") + } + content = content[i:] + start := strings.Index(content, "```go\n") + if start < 0 { + t.Fatal("no Go fence under Handler Implementation") + } + content = content[start+len("```go\n"):] + block := content[:strings.Index(content, "```")] + + path := filepath.Join("docgo", "requestflows", "handler.go") + existing, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + preamble := string(existing)[:strings.Index(string(existing), "func (h *ExchangeHandler) DiscoverResources(")] + formatted, err := format.Source([]byte(preamble + block)) + if err != nil { + t.Fatalf("the example does not parse as Go: %v", err) + } + want := string(formatted) + if os.Getenv("FORA_UPDATE_VECTORS") == "1" { + if err := os.WriteFile(path, []byte(want), 0o644); err != nil { + t.Fatal(err) + } + return + } + if string(existing) != want { + t.Fatalf("%s differs from the example on the page; regenerate with FORA_UPDATE_VECTORS=1", path) + } +} diff --git a/docs/design-history.md b/docs/design-history.md index c291a577..08a9862d 100644 --- a/docs/design-history.md +++ b/docs/design-history.md @@ -668,6 +668,13 @@ contiguous, with no gaps or reused numbers left by the removals. ## Delegation-claims profile: opaque token, bound holder +> **Superseded in part — the claim set is fixed, not open.** "Everything in the +> vocabulary is optional" below no longer holds. The profile requires `iss`, `aud`, +> `cnf.jkt` and a finite `exp` on every token and the protected header `typ` +> `fora-delegation+jwt`; a claim the profile does not list is refused, and vendor +> metadata travels only inside the signed `fora_advisory` object. See "The delegation +> chain is one nested token with a fixed profile". +> > **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, @@ -721,6 +728,16 @@ default.) ## One normative scope-matching algorithm +> **Superseded in part — the SDK helpers now implement the rule.** `ScopesSubset` was +> an exact set check and called itself the attenuation rule; it is now grant +> containment under this algorithm, and `ScopeCovers` / `ScopesCover` expose the rule +> itself in all three SDKs, pinned to a hand-written `scope-match-vectors.json`. Two +> precisions were added while writing the vectors: a terminal `*` needs at least one +> remaining segment to match (`dist:US:*` does not cover `dist:US`), and a `*` in the +> required scope is a literal segment, which is what stops a delegation child from +> widening its parent. See "The delegation chain is one nested token with a fixed +> profile". + > **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, @@ -780,6 +797,11 @@ minimal and its signed offers stay price-comparable. ## JWT (holder-of-key) is the default delegation token; Biscuit is optional +> **Superseded in part — how the chain travels and who is trusted.** The chain is +> nested, not listed: each child carries its complete parent in `fora_parent`, and the +> root verifies with a key configured on the subscription record, never a published +> one. See "The delegation chain is one nested token with a fixed profile". +> > **Biscuit removed pre-v1; superseded by JWT.** This section records the > transition from Biscuit-default to JWT-default. The optional `biscuit-v3` > profile it leaves in place was itself dropped before v1 shipped: JWT is now the @@ -3152,3 +3174,95 @@ checker does: the models are dataclass transforms, and mypy reports the unexpect argument. A model built with `model_construct` skips validation and still drops an undeclared keyword, but it renders a nested value exactly as given. The check of the stamped message therefore refuses an unknown member inside such a value. + +## The delegation chain is one nested token with a fixed profile + +The holder-bound JWT chain was specified loosely: linkage by the child's `jwk` header +alone, every claim optional except `cnf`, `alg` `EdDSA` only, no size or depth limit, +and placeholder tokens in the documentation that no test could verify. An Exchange +about to verify delegations for subscription-gated terms needs one profile a verifier +can be written from, so the profile is now normative on the authentication page, the +Go SDK carries the verifier, and the shared chain cases are derived from the page. + +**The parent travels inside the child.** A child carries its complete signed parent in +the `fora_parent` claim. The alternative, a list of tokens the verifier re-links by +`jwk` and `cnf.jkt`, lets a child be presented under any parent whose `cnf.jkt` names +the same principal key, so one principal's grant from issuer A could carry a child +minted under issuer B's grant. Embedding the parent puts the parent's signature inside +the child's signed payload: a child is bound to one specific parent grant, and +verification is still offline, because every token the verifier needs is inside the +one it was given. The price is size, so the chain is at most four tokens and the +complete token at most 32768 bytes, a wire rule checked before any parsing and again +on every embedded parent before it is decoded. + +**Required claims, and nothing unknown.** `iss`, `aud`, `cnf.jkt` and a finite `exp` +are required, and the protected header `typ` is `fora-delegation+jwt`. `aud` is +required because a token minted for one Exchange must not be presentable to another +that trusts the same issuer; it follows the Request recipient rule the `exchange` +field already uses, so the two identities are compared the same way. `exp` is +required because an open-ended grant has no bound a verifier can return. `typ` keeps +an access token or an ID token from the same issuer from being read as a delegation. +A claim the profile does not list is refused: the earlier text let a verifier skip a +claim the issuer marked advisory, which means the issuer decides what the verifier may +ignore; now vendor metadata travels only inside the signed `fora_advisory` object, +which may not carry the name of a profile claim. The clock leeway is one number, 60 +seconds, with the comparisons written out, because two verifiers that disagree by a +second at a boundary disagree on the vectors. + +**Intermediate issuers name their key.** A child's `iss` is the RFC 7638 thumbprint +of its own signing key, which is also the parent's `cnf.jkt`. Any other rule would +need an input the verifier does not have, a registry of intermediate identities; this +one is checkable from the chain alone, and it makes a child whose signature and key +linkage are valid but whose `iss` claims to be someone else a refusal. + +**Trust is configured, never discovered.** The root verifies with a key the Exchange +has configured for the issuer's identity, on the subscription record that owns the +gated term. `Delegation.issuer` once pointed at an OIDC issuer URL for a JWKS lookup; +a token-supplied URL is the token choosing its own verifier, and a directory fetch is a +network dependency on the issuer at request time. Both are gone. `issuer` and +`principal_domain` are mirrors of the root's `iss`, the trust anchor's identity, +never an intermediate issuer, and establish nothing; the other plaintext fields mirror +the terminal token's effective grant and cannot widen it. Removing a key from the +record is the explicit withdrawal of trust; per-token revocation through +`revocation_uri` is not part of this profile. + +**Caps are refused by the verifier of this release.** `fora_max_spend_cents`, +`fora_max_accesses` and `fora_quota_period` stay profile claims, but the Go verifier +meters nothing, so a chain that carries one is refused rather than the cap dropped, +which is what fail-closed means for a restriction. An Exchange that enforces caps +verifies them in its own step. A gated offer never expires later than the chain's +effective expiry, the earliest `exp` without leeway, which the verifier returns. + +**The documentation shows real tokens.** Every documented token is a case in the +shared chain file, named on the line above its fence, and a conformance test verifies +it cryptographically under that case's inputs. A page can no longer show a chain that +does not verify. + +## The delegation verifier checks Ed25519 itself, under either algorithm name + +The holder-bound delegation profile accepts two protected-header `alg` values for one +key type: `EdDSA`, the polymorphic name RFC 8037 registered, and `Ed25519`, the +fully-specified name RFC 9864 registered in 2025 while marking `EdDSA` deprecated. Current +issuers (panva `jose` from 5.10, `joserfc`) emit `Ed25519`; older ones emit `EdDSA`. A +verifier that accepted only one name would refuse valid chains from the other half. + +**go-jose cannot be the signature check.** The pinned go-jose v4 knows only `EdDSA`. A +token whose header says `Ed25519` is refused by its parser before any key is inspected, +even when `Ed25519` is placed in the allowed-algorithms list, and its main branch has no +such algorithm either. Rewriting the header to `EdDSA` before handing the token over would +change the signed bytes, so that is not an option. + +**The check is small enough to own.** A compact JWS is three base64url segments; the +signing input is the first two joined by a dot; an Ed25519 signature is 64 bytes; +`crypto/ed25519` verifies it in one call. `helpers.VerifyDelegation` therefore splits the +token, reads the protected header, and verifies the signature with the standard library +over the original bytes, under either `alg` name. go-jose keeps the two jobs it does well: +it parses the protected `jwk` header, where its OKP decoder refuses any curve but Ed25519, +and it computes the RFC 7638 thumbprint through `helpers.Thumbprint`, as it already did. + +**One verifier, in Go, for now.** The Exchange imports this verifier; the TypeScript and +Python ports are deferred and recorded as a decision in the parity matrix, so the gap is +visible rather than silent. The chain cases the Go tests replay are kept outside the +`*-vectors.json` convention until those ports exist, because that convention is a promise +of tri-language replay and the completeness gate enforces it without exemption. + diff --git a/docs/sdk-parity-matrix.md b/docs/sdk-parity-matrix.md index 9479f4f6..63be5882 100644 --- a/docs/sdk-parity-matrix.md +++ b/docs/sdk-parity-matrix.md @@ -12,7 +12,7 @@ Go is the oracle (`sdk/go/{helpers,resolvers,core,connect,connectserver,profiles/music,helperstest}`); Python and TS mirror it. This document is **generated** from the same two artifacts CI already enforces against the code, so it cannot drift from the real surface — a mismatch fails the API-surface gate or the corpus-completeness gate before it can reach this file. -**At a glance:** 196 symbols at cross-language parity · 17 documented divergences · 229 Go-idiomatic exclusions · 46 conformance corpora, each tri-replayed. +**At a glance:** 198 symbols at cross-language parity · 29 documented divergences · 252 Go-idiomatic exclusions · 47 conformance corpora, each tri-replayed. Layering (L1 pure trust core vs L2 I/O resolvers), the SSRF transport-wiring invariant, and naming conventions are recorded in [`design-history.md`](./design-history.md). @@ -115,6 +115,8 @@ Legend: a name = the public face in that language · `—` = intentionally none | `RuleViolation` | `RuleViolation` | `RuleViolation` | | `RuleWarning` | `RuleWarning` | `RuleWarning` | | `SchemaVerdict` | `SchemaVerdict` | `SchemaVerdict` | +| `ScopeCovers` | `scope_covers` | `scopeCovers` | +| `ScopesCover` | `scopes_cover` | `scopesCover` | | `ScopesSubset` | `scopes_subset` | `scopesSubset` | | `SignAgentBinding` | `sign_agent_binding` | `signInbound` | | `SignDirectoryResponse` | `sign_directory_response` | `signDirectoryResponse` | @@ -257,6 +259,7 @@ Deliberate, reason-backed asymmetries. The allowlist is **shrink-only** — a ne ### Architectural DECISIONs +- **DECISION — delegation verifier.** Go-only delegation chain verifier — OPEN DECISION in docs/sdk-parity-matrix.md: the holder-bound JWT profile ships with one verifier, in Go, that the Exchange imports; the TypeScript and Python verifiers are deferred and tracked as a FORA-84 follow-up, and the chain cases in sdk/go/helpers/testdata/delegation-chains.json become a tri-replayed *-vectors.json corpus when they land. The mirrors a case carries (the plaintext Delegation fields next to the token) are replayed only by an Exchange's own tests; nothing in this repository reads them _(symbols: `helpers.DelegationClockLeeway`, `helpers.DelegationError`, `helpers.DelegationFailure`, `helpers.DelegationInput`, `helpers.DelegationTokenType`, `helpers.ErrDelegationInvalid`, `helpers.MaxDelegationChainDepth`, `helpers.MaxDelegationNumericDate`, `helpers.MaxDelegationTokenBytes`, `helpers.TrustedIssuer`, `helpers.VerifiedDelegation`, `helpers.VerifyDelegation`)_ - **DECISION — full Connect handler binding.** Go-only full Connect Broker handler binding — OPEN DECISION in docs/sdk-parity-matrix.md _(symbols: `connectserver.NewBrokerServiceHandler`, `connectserver.NewCatalogServiceHandler`, `connectserver.NewExchangeServiceHandler`)_ ### Mapped symbols with an intentional per-language gap @@ -271,7 +274,19 @@ Deliberate, reason-backed asymmetries. The allowlist is **shrink-only** — a ne | `connectserver.NewCatalogServiceHandler` | — | — | Go-only full Connect Catalog handler binding — OPEN DECISION in docs/sdk-parity-matrix.md | | `connectserver.NewExchangeServiceHandler` | — | — | Go-only full Connect Exchange handler binding — OPEN DECISION in docs/sdk-parity-matrix.md | | `core.NewVerifier` | — | `createVerifier` | Go NewVerifier factory folds into the Python class constructor (Verifier(...)); idiomatic Python exposes the class, not a separate factory symbol. TS keeps the createVerifier factory. | +| `helpers.DelegationClockLeeway` | — | — | Go-only delegation chain verifier — OPEN DECISION in docs/sdk-parity-matrix.md: the holder-bound JWT profile ships with one verifier, in Go, that the Exchange imports; the TypeScript and Python verifiers are deferred and tracked as a FORA-84 follow-up, and the chain cases in sdk/go/helpers/testdata/delegation-chains.json become a tri-replayed *-vectors.json corpus when they land. The mirrors a case carries (the plaintext Delegation fields next to the token) are replayed only by an Exchange's own tests; nothing in this repository reads them | +| `helpers.DelegationError` | — | — | Go-only delegation chain verifier — OPEN DECISION in docs/sdk-parity-matrix.md: the holder-bound JWT profile ships with one verifier, in Go, that the Exchange imports; the TypeScript and Python verifiers are deferred and tracked as a FORA-84 follow-up, and the chain cases in sdk/go/helpers/testdata/delegation-chains.json become a tri-replayed *-vectors.json corpus when they land. The mirrors a case carries (the plaintext Delegation fields next to the token) are replayed only by an Exchange's own tests; nothing in this repository reads them | +| `helpers.DelegationFailure` | — | — | Go-only delegation chain verifier — OPEN DECISION in docs/sdk-parity-matrix.md: the holder-bound JWT profile ships with one verifier, in Go, that the Exchange imports; the TypeScript and Python verifiers are deferred and tracked as a FORA-84 follow-up, and the chain cases in sdk/go/helpers/testdata/delegation-chains.json become a tri-replayed *-vectors.json corpus when they land. The mirrors a case carries (the plaintext Delegation fields next to the token) are replayed only by an Exchange's own tests; nothing in this repository reads them | +| `helpers.DelegationInput` | — | — | Go-only delegation chain verifier — OPEN DECISION in docs/sdk-parity-matrix.md: the holder-bound JWT profile ships with one verifier, in Go, that the Exchange imports; the TypeScript and Python verifiers are deferred and tracked as a FORA-84 follow-up, and the chain cases in sdk/go/helpers/testdata/delegation-chains.json become a tri-replayed *-vectors.json corpus when they land. The mirrors a case carries (the plaintext Delegation fields next to the token) are replayed only by an Exchange's own tests; nothing in this repository reads them | +| `helpers.DelegationTokenType` | — | — | Go-only delegation chain verifier — OPEN DECISION in docs/sdk-parity-matrix.md: the holder-bound JWT profile ships with one verifier, in Go, that the Exchange imports; the TypeScript and Python verifiers are deferred and tracked as a FORA-84 follow-up, and the chain cases in sdk/go/helpers/testdata/delegation-chains.json become a tri-replayed *-vectors.json corpus when they land. The mirrors a case carries (the plaintext Delegation fields next to the token) are replayed only by an Exchange's own tests; nothing in this repository reads them | +| `helpers.ErrDelegationInvalid` | — | — | Go-only delegation chain verifier — OPEN DECISION in docs/sdk-parity-matrix.md: the holder-bound JWT profile ships with one verifier, in Go, that the Exchange imports; the TypeScript and Python verifiers are deferred and tracked as a FORA-84 follow-up, and the chain cases in sdk/go/helpers/testdata/delegation-chains.json become a tri-replayed *-vectors.json corpus when they land. The mirrors a case carries (the plaintext Delegation fields next to the token) are replayed only by an Exchange's own tests; nothing in this repository reads them | +| `helpers.MaxDelegationChainDepth` | — | — | Go-only delegation chain verifier — OPEN DECISION in docs/sdk-parity-matrix.md: the holder-bound JWT profile ships with one verifier, in Go, that the Exchange imports; the TypeScript and Python verifiers are deferred and tracked as a FORA-84 follow-up, and the chain cases in sdk/go/helpers/testdata/delegation-chains.json become a tri-replayed *-vectors.json corpus when they land. The mirrors a case carries (the plaintext Delegation fields next to the token) are replayed only by an Exchange's own tests; nothing in this repository reads them | +| `helpers.MaxDelegationNumericDate` | — | — | Go-only delegation chain verifier — OPEN DECISION in docs/sdk-parity-matrix.md: the holder-bound JWT profile ships with one verifier, in Go, that the Exchange imports; the TypeScript and Python verifiers are deferred and tracked as a FORA-84 follow-up, and the chain cases in sdk/go/helpers/testdata/delegation-chains.json become a tri-replayed *-vectors.json corpus when they land. The mirrors a case carries (the plaintext Delegation fields next to the token) are replayed only by an Exchange's own tests; nothing in this repository reads them | +| `helpers.MaxDelegationTokenBytes` | — | — | Go-only delegation chain verifier — OPEN DECISION in docs/sdk-parity-matrix.md: the holder-bound JWT profile ships with one verifier, in Go, that the Exchange imports; the TypeScript and Python verifiers are deferred and tracked as a FORA-84 follow-up, and the chain cases in sdk/go/helpers/testdata/delegation-chains.json become a tri-replayed *-vectors.json corpus when they land. The mirrors a case carries (the plaintext Delegation fields next to the token) are replayed only by an Exchange's own tests; nothing in this repository reads them | | `helpers.NewStaticKeyResolver` | — | `createStaticKeyResolver` | Go NewStaticKeyResolver factory folds into the Python class constructor (StaticKeyResolver(...)); idiomatic Python exposes the class, not a separate factory symbol. TS keeps the createStaticKeyResolver factory. | +| `helpers.TrustedIssuer` | — | — | Go-only delegation chain verifier — OPEN DECISION in docs/sdk-parity-matrix.md: the holder-bound JWT profile ships with one verifier, in Go, that the Exchange imports; the TypeScript and Python verifiers are deferred and tracked as a FORA-84 follow-up, and the chain cases in sdk/go/helpers/testdata/delegation-chains.json become a tri-replayed *-vectors.json corpus when they land. The mirrors a case carries (the plaintext Delegation fields next to the token) are replayed only by an Exchange's own tests; nothing in this repository reads them | +| `helpers.VerifiedDelegation` | — | — | Go-only delegation chain verifier — OPEN DECISION in docs/sdk-parity-matrix.md: the holder-bound JWT profile ships with one verifier, in Go, that the Exchange imports; the TypeScript and Python verifiers are deferred and tracked as a FORA-84 follow-up, and the chain cases in sdk/go/helpers/testdata/delegation-chains.json become a tri-replayed *-vectors.json corpus when they land. The mirrors a case carries (the plaintext Delegation fields next to the token) are replayed only by an Exchange's own tests; nothing in this repository reads them | +| `helpers.VerifyDelegation` | — | — | Go-only delegation chain verifier — OPEN DECISION in docs/sdk-parity-matrix.md: the holder-bound JWT profile ships with one verifier, in Go, that the Exchange imports; the TypeScript and Python verifiers are deferred and tracked as a FORA-84 follow-up, and the chain cases in sdk/go/helpers/testdata/delegation-chains.json become a tri-replayed *-vectors.json corpus when they land. The mirrors a case carries (the plaintext Delegation fields next to the token) are replayed only by an Exchange's own tests; nothing in this repository reads them | | `resolvers.CachedOfferKeyResolverConfig` | — | `CachedOfferKeyResolverOptions` | Go config struct / TS options object folds into Python constructor keyword arguments (CachedOfferKeyResolver(...)); idiomatic Python has no separate options type. | | `resolvers.NewCachedOfferKeyResolver` | — | `createCachedOfferKeyResolver` | Go NewCachedOfferKeyResolver factory folds into the Python class constructor (CachedOfferKeyResolver(...)); idiomatic Python exposes the class, not a separate factory symbol. TS keeps the createCachedOfferKeyResolver factory. | | `resolvers.NewWBAKeyResolver` | — | `createWBAKeyResolver` | Go NewWBAKeyResolver factory folds into the Python class constructor (WBAKeyResolver(...)); idiomatic Python exposes the class, not a separate factory symbol. TS keeps the createWBAKeyResolver factory. | @@ -380,6 +395,29 @@ Go constructs (functional-option builders, `errors.Is` sentinels, value types, c | `helpers.CheckStrictMessage` | Go check of an already-decoded proto.Message, which is how the Go client holds a binary answer and every ErrorDetail; py/ts hold only proto-JSON, so check_strict/checkStrict over the decoded JSON is the whole of their check and there is no message object to pass. | | `helpers.ComponentParam` | Go value type for an RFC 9421 covered-component parameter; py/ts model components inline. | | `helpers.CoveredComponent` | Go value type for an RFC 9421 covered component; py/ts model components inline. | +| `helpers.DelegationAdvisoryShadowsClaim` | Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens. | +| `helpers.DelegationAudienceMismatch` | Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens. | +| `helpers.DelegationAudienceWidened` | Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens. | +| `helpers.DelegationBadAlg` | Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens. | +| `helpers.DelegationBadClaim` | Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens. | +| `helpers.DelegationBadJWK` | Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens. | +| `helpers.DelegationBadSignature` | Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens. | +| `helpers.DelegationBadTyp` | Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens. | +| `helpers.DelegationChildIssuerMismatch` | Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens. | +| `helpers.DelegationCritPresent` | Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens. | +| `helpers.DelegationExpired` | Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens. | +| `helpers.DelegationExpiryWidened` | Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens. | +| `helpers.DelegationHolderMismatch` | Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens. | +| `helpers.DelegationJWKMismatch` | Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens. | +| `helpers.DelegationMalformed` | Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens. | +| `helpers.DelegationMissingClaim` | Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens. | +| `helpers.DelegationNotYetValid` | Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens. | +| `helpers.DelegationOversized` | Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens. | +| `helpers.DelegationScopeWidened` | Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens. | +| `helpers.DelegationTooDeep` | Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens. | +| `helpers.DelegationUnknownClaim` | Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens. | +| `helpers.DelegationUnsupportedRestriction` | Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens. | +| `helpers.DelegationUntrustedIssuer` | Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens. | | `helpers.ErrAcceptanceRequesterEmpty` | Go errors.Is sentinel for an acceptance that names an empty requester; py raises ValueError and ts throws from the canonical-bytes and signing faces, and the shared refused-acceptance vectors pin that every language refuses the same inputs. | | `helpers.ErrAcceptanceSignatureInvalid` | Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels. | | `helpers.ErrAudienceIdentity` | Go errors.Is sentinel for an unusable configured Exchange identity; py/ts raise/throw instead of exporting sentinels. | @@ -539,6 +577,7 @@ Go emits each `*-vectors.json` oracle; Python and TS replay it. The completeness | `helpers/testdata/pop-vectors.json` | ✅ | ✅ | ✅ | | `helpers/testdata/registration-schema-vectors.json` | ✅ | ✅ | ✅ | | `helpers/testdata/request-acceptance-vectors.json` | ✅ | ✅ | ✅ | +| `helpers/testdata/scope-match-vectors.json` | ✅ | ✅ | ✅ | | `helpers/testdata/scopes-vectors.json` | ✅ | ✅ | ✅ | | `helpers/testdata/sign-request-vectors.json` | ✅ | ✅ | ✅ | | `helpers/testdata/signedurl-vectors.json` | ✅ | ✅ | ✅ | diff --git a/gen/descriptor.binpb b/gen/descriptor.binpb index e83839c9..87ed02b8 100644 Binary files a/gen/descriptor.binpb and b/gen/descriptor.binpb differ diff --git a/gen/go/fora/v1/fora.pb.go b/gen/go/fora/v1/fora.pb.go index e78fa206..74ab5ee6 100644 --- a/gen/go/fora/v1/fora.pb.go +++ b/gen/go/fora/v1/fora.pb.go @@ -937,7 +937,7 @@ const ( // is denied and the whole request is not refused. DenialReason_DENIAL_REASON_SIGNATURE_INVALID DenialReason = 8 DenialReason_DENIAL_REASON_QUOTA_EXCEEDED DenialReason = 9 // Subscription access count exhausted for this period - DenialReason_DENIAL_REASON_DELEGATION_INVALID DenialReason = 10 // Delegation missing, unverifiable, expired, holder binding failed, or scopes/caps do not cover the request + DenialReason_DENIAL_REASON_DELEGATION_INVALID DenialReason = 10 // On a purchase item: delegation missing, malformed, unverifiable (chain linkage, issuer, signature), expired or not yet valid, addressed to another recipient, holder binding failed, a child widens its parent, or scopes/caps do not cover the request. A token over the size limit is refused at wire validation, before any item; at discovery a failed delegation hides the gated terms and reports nothing // DEPRECATED, never sent. An Exchange MUST NOT send this value. An offer the // Exchange presented is honoured until it expires, so a purchase is never // refused for scope. A resource the requester's scopes leave with no @@ -2813,7 +2813,9 @@ type Offer struct { DeliveryMethod DeliveryMethod `protobuf:"varint,4,opt,name=delivery_method,json=deliveryMethod,proto3,enum=fora.v1.DeliveryMethod" json:"delivery_method,omitempty"` // Post-usage reporting requirements for this offer. Reporting *ReportingObligation `protobuf:"bytes,5,opt,name=reporting,proto3,oneof" json:"reporting,omitempty"` - // When this offer expires (ISO 8601). + // When this offer expires (ISO 8601). An offer for a term gated behind a + // verified delegation MUST NOT expire later than that delegation chain's + // effective expiry, the earliest `exp` in the chain (see Delegation). ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=expires_at,json=expiresAt,proto3,oneof" json:"expires_at,omitempty"` // Resource identity for cross-exchange deduplication. // Enables Brokers to recognize the same resource offered by @@ -4074,16 +4076,20 @@ type LicenseTerm struct { // Requester.scopes is the requester's own statement, which the Exchange does // not verify, so a scope gate hides a term from requesters that do not state // the scope; it does not stop one that states it falsely. An Exchange MAY - // also require a verified delegation that grants these scopes before it - // presents the term. To a requester that presents none, it answers as if the - // term did not exist, as existence hiding requires (see Requester.scopes). An - // offer the Exchange presented is honoured until it expires. + // instead gate a term behind a verified delegation (a subscription term is + // the usual case): then only the delegation chain's verified effective scopes + // are matched against these scopes, and Requester.scopes is ignored for that + // term. To a requester that presents no valid delegation, it answers as if + // the term did not exist, as existence hiding requires (see + // Requester.scopes). An offer the Exchange presented is honoured until it + // expires, and a gated offer expires no later than the chain's effective + // expiry (see Delegation). // // Coverage uses the SAME matching rule as Requester.scopes: // segment-wise (":" separated), each granted segment must equal the // corresponding required segment or be "*", a terminal "*" matches all - // remaining segments, and there is NO implicit prefix match (a grant - // narrower than the requirement does not cover it). "dist:*" covers + // remaining segments (at least one), and there is NO implicit prefix match + // (a grant narrower than the requirement does not cover it). "dist:*" covers // "dist:US" and "dist:US:CA"; "dist" covers only "dist". There is exactly // one scope-matching algorithm across the protocol. Scopes []string `protobuf:"bytes,7,rep,name=scopes,proto3" json:"scopes,omitempty"` @@ -4558,12 +4564,18 @@ type Requester struct { // Matching is SEGMENT-WISE (":" separated). A granted scope G covers a // required scope R iff, segment by segment, each G segment equals the // corresponding R segment or is "*"; a terminal "*" matches all remaining - // segments. There is NO implicit prefix match, and a grant NARROWER than - // the requirement does not cover it (G must be equal-to-or-broader than R). - // Examples: "dist:*" covers "dist:US" and "dist:US:CA"; "dist:US:*" covers - // "dist:US:CA" but not "dist:EU"; bare "dist" covers only "dist"; granted - // "dist:US:CA" does NOT cover required "dist:US"; "*" covers everything. - // This same rule governs LicenseTerm.scopes — one algorithm protocol-wide. + // segments, of which there must be at least one. There is NO implicit prefix + // match, and a grant NARROWER than the requirement does not cover it (G must + // be equal-to-or-broader than R), so a grant with more segments than the + // requirement never covers it, even when its last segment is "*". A "*" in R + // is a literal segment: a narrow grant never covers a wildcard requirement. + // Matching is case-sensitive and never trims. Examples: "dist:*" covers + // "dist:US", "dist:US:CA" and "dist:US:*" but not "dist"; "dist:US:*" covers + // "dist:US:CA" but not "dist:EU" or "dist:US"; "publisher:*:read" covers + // "publisher:news:read" but not "publisher:news:*"; bare "dist" covers only + // "dist"; granted "dist:US:CA" does NOT cover required "dist:US"; "*" covers + // everything. This same rule governs LicenseTerm.scopes and the attenuation + // of a delegation child against its parent — one algorithm protocol-wide. // // When empty, Exchange applies its default access policy (typically // returns all publicly available resources). @@ -4673,60 +4685,41 @@ func (x *Requester) GetExtCritical() []string { // Delegation — Scoped, time-limited, spend-capped credential. // -// The token is an opaque, signed credential. The format is a JWT (token_format -// "jwt"); the field stays open for a future format. Its claims are the -// AUTHORITATIVE source of the grant; the -// plaintext fields below (scopes, expires_at, max_spend_cents, …) are a -// convenience mirror the Exchange MAY use for fast pre-filtering before it -// verifies the token. +// The token is a signed JWT (token_format "jwt"; the field stays open for a +// future format) carrying a holder-bound delegation chain. Its claims are the +// AUTHORITATIVE source of the grant; the plaintext fields below (scopes, +// expires_at, max_spend_cents, …) are a mirror the Exchange MAY use for fast +// pre-filtering before it verifies the token. A mirror establishes nothing: it +// cannot grant a scope, extend validity or raise a cap beyond what the signed +// chain says. // -// Holder binding (the load-bearing property). The grant is bound to a holder key -// via the RFC 7800 `cnf` confirmation claim — `cnf.jkt`, the RFC 7638 JWK -// thumbprint of the holder's key. Verification MUST check that the key signing -// the request (RFC 9421) hashes to that thumbprint; a token possessed without -// the matching private key is rejected. This is what makes a leaked token NOT -// bearer-usable. One exception, the relayed purchase: when a Broker re-packages -// a purchase (BrokerService.ExecuteTransaction), the request an Exchange -// receives is signed by the Broker, not by the holder. There the Exchange checks -// the holder binding against the key each item's AgentAcceptance verifies under -// — the agent's key, resolved from the requester's WBA directory — and not -// against the request signer. A relayed purchase that carries a delegation -// therefore needs an AgentAcceptance on every item; an item without one fails -// the binding (DENIAL_REASON_DELEGATION_INVALID). On a query a Broker -// originated (Broker-led discovery), the Broker signs alone, so a delegation on -// it cannot pass holder binding: the Exchange does not honour it 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. An Exchange -// that requires a verified delegation before it presents a scoped term (see -// LicenseTerm.scopes) therefore presents no such term on that query. -// -// Delegation is a chain of cnf-linked JWTs: a principal narrows a -// grant by issuing a child JWT (cnf = the next holder, scopes ⊆ parent), signed -// by the key the parent's cnf named — the chain-linkage invariant. Verifiers -// need only the root issuer's public key; intermediate keys ride inside the -// chain (JOSE header `jwk`), so verification is offline. -// -// The claim schema is a small registered vocabulary — see the FORA -// delegation-claims profile in the auth spec. All claims are OPTIONAL except the -// holder binding (cnf). Vendors MAY add namespaced claims (their own "vendor:" -// namespace; "fora_"-prefixed names are reserved for the registered vocabulary -// and MUST NOT be redefined); any constraint a verifier cannot evaluate is -// binding by default (fail closed) unless the issuer marks it advisory — -// mirroring Restriction.advisory. -// -// Scope/time/spend caps are defense-in-depth that bound the blast radius only in -// the residual case where the holder's signing key is also compromised; the -// primary protection against theft is the holder binding above. +// The profile — chain encoding and limits, protected header, claims, clock +// leeway, issuer, audience and holder-binding rules, the verification +// procedure and the outcome of each failure — is the "Delegation" section of +// the authentication page and is not restated here; the Go SDK verifier +// (sdk/go/helpers) and the shared chain cases are derived from it. What a +// field reader must know: the grant is bound to a holder key by `cnf.jkt`, and +// the key a request proves possession of MUST hash to the terminal token's +// `cnf.jkt` (the RFC 9421 request signer on a direct call; on a purchase a +// Broker relayed, the key each item's AgentAcceptance verifies under), so a +// token possessed without its private key grants nothing. On a query a Broker +// originated nothing can pass that binding: the Exchange does not honour the +// delegation, answers as if none were present, and presents no term gated +// behind one (see LicenseTerm.scopes). type Delegation struct { state protoimpl.MessageState `protogen:"open.v1"` - // Who granted this delegation (domain for public key lookup). + // Mirror of the ROOT token's `iss`, the configured trust anchor's identity. + // Informational: no key is looked up from it, and it establishes no trust. PrincipalDomain string `protobuf:"bytes,1,opt,name=principal_domain,json=principalDomain,proto3" json:"principal_domain,omitempty"` // Principal's identifier (e.g., "user@acme.com", "marketdata.example.com"). + // Informational; not read by the verifier. PrincipalId string `protobuf:"bytes,2,opt,name=principal_id,json=principalId,proto3" json:"principal_id,omitempty"` - // Scopes granted by this delegation. MUST be a subset of the - // principal's own scopes (attenuation — can only narrow, not widen). + // Mirror of the terminal token's effective scopes. In the chain, each + // child's scopes are covered by its parent's under the scope-matching rule + // on Requester.scopes (attenuation — can only narrow, not widen). Scopes []string `protobuf:"bytes,3,rep,name=scopes,proto3" json:"scopes,omitempty"` - // When this delegation expires. Exchange MUST reject expired tokens. + // Mirror of the chain's earliest `exp`. The Exchange MUST reject an expired + // token; the signed `exp` values decide, not this field. ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` // Maximum spend in currency minor units (e.g., cents for USD). // Exchange tracks cumulative spend against this cap. @@ -4741,18 +4734,22 @@ type Delegation struct { // (proto-JSON encodes Duration as seconds; "720h" is not accepted). // When absent, the quota is lifetime (bounded only by expires_at). QuotaPeriod *durationpb.Duration `protobuf:"bytes,10,opt,name=quota_period,json=quotaPeriod,proto3,oneof" json:"quota_period,omitempty"` - // Token bytes. A JWT (base64url-encoded JWS). + // Token bytes. A JWT (base64url-encoded JWS). At most 32768 bytes: the + // complete chain, every embedded parent included, must fit. The limit is a + // wire rule, checked before the token is parsed and independent of whether + // the bytes are a valid JWT; a longer token is an invalid request, not a + // per-item denial. Token []byte `protobuf:"bytes,6,opt,name=token,proto3" json:"token,omitempty"` // Token format: "jwt" (default). Empty is treated as "jwt". The field stays // open for a future format. TokenFormat string `protobuf:"bytes,7,opt,name=token_format,json=tokenFormat,proto3" json:"token_format,omitempty"` - // Optional: URI for real-time revocation checking. - // Exchange MAY check this for high-value transactions. - // Not checked for routine low-value access (performance tradeoff). + // Informational. Revocation lookup is not part of the delegation profile: + // the verifier never fetches this URI. A chain is invalidated early by + // withdrawing the configured root key. RevocationUri *string `protobuf:"bytes,8,opt,name=revocation_uri,json=revocationUri,proto3,oneof" json:"revocation_uri,omitempty"` - // Token issuer. OIDC issuer URL or GNAP grant server URL. - // Exchange uses this for JWT validation (OIDC discovery → JWKS) - // or GNAP token introspection. + // Mirror of the ROOT token's `iss`, the identity configured with the trusted + // key (never an intermediate or terminal issuer). Informational: the + // Exchange never discovers a key from it, through OIDC, JWKS or otherwise. Issuer *string `protobuf:"bytes,11,opt,name=issuer,proto3,oneof" json:"issuer,omitempty"` // Extension point Ext *structpb.Struct `protobuf:"bytes,15,opt,name=ext,proto3" json:"ext,omitempty"` @@ -11220,7 +11217,7 @@ const file_fora_v1_fora_proto_rawDesc = "" + "\x03ext\x18\x0f \x01(\v2\x17.google.protobuf.StructR\x03ext\x12!\n" + "\fext_critical\x18Z \x03(\tR\vextCriticalB\a\n" + "\x05_nameB\r\n" + - "\v_delegation\"\xe9\x04\n" + + "\v_delegation\"\xf4\x04\n" + "\n" + "Delegation\x12)\n" + "\x10principal_domain\x18\x01 \x01(\tR\x0fprincipalDomain\x12!\n" + @@ -11231,8 +11228,8 @@ const file_fora_v1_fora_proto_rawDesc = "" + "\x0fmax_spend_cents\x18\x05 \x01(\x03H\x00R\rmaxSpendCents\x88\x01\x01\x12&\n" + "\fmax_accesses\x18\t \x01(\x05H\x01R\vmaxAccesses\x88\x01\x01\x12A\n" + "\fquota_period\x18\n" + - " \x01(\v2\x19.google.protobuf.DurationH\x02R\vquotaPeriod\x88\x01\x01\x12\x14\n" + - "\x05token\x18\x06 \x01(\fR\x05token\x12!\n" + + " \x01(\v2\x19.google.protobuf.DurationH\x02R\vquotaPeriod\x88\x01\x01\x12\x1f\n" + + "\x05token\x18\x06 \x01(\fB\t\xbaH\x06z\x04\x18\x80\x80\x02R\x05token\x12!\n" + "\ftoken_format\x18\a \x01(\tR\vtokenFormat\x12*\n" + "\x0erevocation_uri\x18\b \x01(\tH\x03R\rrevocationUri\x88\x01\x01\x12\x1b\n" + "\x06issuer\x18\v \x01(\tH\x04R\x06issuer\x88\x01\x01\x12)\n" + diff --git a/gen/jsonschema/fora.v1.Delegation.schema.json b/gen/jsonschema/fora.v1.Delegation.schema.json index 54c2ca8f..abbc396c 100644 --- a/gen/jsonschema/fora.v1.Delegation.schema.json +++ b/gen/jsonschema/fora.v1.Delegation.schema.json @@ -1,10 +1,10 @@ { "$id": "fora.v1.Delegation.schema.json", "$schema": "https://json-schema.org/draft/2020-12/schema", - "description": "The token is an opaque, signed credential. The format is a JWT (token_format\n \"jwt\"); the field stays open for a future format. Its claims are the\n AUTHORITATIVE source of the grant; the\n plaintext fields below (scopes, expires_at, max_spend_cents, …) are a\n convenience mirror the Exchange MAY use for fast pre-filtering before it\n verifies the token.\n\n Holder binding (the load-bearing property). The grant is bound to a holder key\n via the RFC 7800 `cnf` confirmation claim — `cnf.jkt`, the RFC 7638 JWK\n thumbprint of the holder's key. Verification MUST check that the key signing\n the request (RFC 9421) hashes to that thumbprint; a token possessed without\n the matching private key is rejected. This is what makes a leaked token NOT\n bearer-usable. One exception, the relayed purchase: when a Broker re-packages\n a purchase (BrokerService.ExecuteTransaction), the request an Exchange\n receives is signed by the Broker, not by the holder. There the Exchange checks\n the holder binding against the key each item's AgentAcceptance verifies under\n — the agent's key, resolved from the requester's WBA directory — and not\n against the request signer. A relayed purchase that carries a delegation\n therefore needs an AgentAcceptance on every item; an item without one fails\n the binding (DENIAL_REASON_DELEGATION_INVALID). On a query a Broker\n originated (Broker-led discovery), the Broker signs alone, so a delegation on\n it cannot pass holder binding: the Exchange does not honour it and answers\n as if none were present. How an agent proves a holder-of-key entitlement at\n discovery time through a Broker is an open protocol question. An Exchange\n that requires a verified delegation before it presents a scoped term (see\n LicenseTerm.scopes) therefore presents no such term on that query.\n\n Delegation is a chain of cnf-linked JWTs: a principal narrows a\n grant by issuing a child JWT (cnf = the next holder, scopes ⊆ parent), signed\n by the key the parent's cnf named — the chain-linkage invariant. Verifiers\n need only the root issuer's public key; intermediate keys ride inside the\n chain (JOSE header `jwk`), so verification is offline.\n\n The claim schema is a small registered vocabulary — see the FORA\n delegation-claims profile in the auth spec. All claims are OPTIONAL except the\n holder binding (cnf). Vendors MAY add namespaced claims (their own \"vendor:\"\n namespace; \"fora_\"-prefixed names are reserved for the registered vocabulary\n and MUST NOT be redefined); any constraint a verifier cannot evaluate is\n binding by default (fail closed) unless the issuer marks it advisory —\n mirroring Restriction.advisory.\n\n Scope/time/spend caps are defense-in-depth that bound the blast radius only in\n the residual case where the holder's signing key is also compromised; the\n primary protection against theft is the holder binding above.", + "description": "The token is a signed JWT (token_format \"jwt\"; the field stays open for a\n future format) carrying a holder-bound delegation chain. Its claims are the\n AUTHORITATIVE source of the grant; the plaintext fields below (scopes,\n expires_at, max_spend_cents, …) are a mirror the Exchange MAY use for fast\n pre-filtering before it verifies the token. A mirror establishes nothing: it\n cannot grant a scope, extend validity or raise a cap beyond what the signed\n chain says.\n\n The profile — chain encoding and limits, protected header, claims, clock\n leeway, issuer, audience and holder-binding rules, the verification\n procedure and the outcome of each failure — is the \"Delegation\" section of\n the authentication page and is not restated here; the Go SDK verifier\n (sdk/go/helpers) and the shared chain cases are derived from it. What a\n field reader must know: the grant is bound to a holder key by `cnf.jkt`, and\n the key a request proves possession of MUST hash to the terminal token's\n `cnf.jkt` (the RFC 9421 request signer on a direct call; on a purchase a\n Broker relayed, the key each item's AgentAcceptance verifies under), so a\n token possessed without its private key grants nothing. On a query a Broker\n originated nothing can pass that binding: the Exchange does not honour the\n delegation, answers as if none were present, and presents no term gated\n behind one (see LicenseTerm.scopes).", "properties": { "expires_at": { - "description": "When this delegation expires. Exchange MUST reject expired tokens.", + "description": "Mirror of the chain's earliest `exp`. The Exchange MUST reject an expired\n token; the signed `exp` values decide, not this field.", "format": "date-time", "type": "string" }, @@ -20,7 +20,7 @@ "type": "array" }, "issuer": { - "description": "Token issuer. OIDC issuer URL or GNAP grant server URL.\n Exchange uses this for JWT validation (OIDC discovery → JWKS)\n or GNAP token introspection.", + "description": "Mirror of the ROOT token's `iss`, the identity configured with the trusted\n key (never an intermediate or terminal issuer). Informational: the\n Exchange never discovers a key from it, through OIDC, JWKS or otherwise.", "type": "string" }, "max_accesses": { @@ -43,12 +43,12 @@ }, "principal_domain": { "default": "", - "description": "Who granted this delegation (domain for public key lookup).", + "description": "Mirror of the ROOT token's `iss`, the configured trust anchor's identity.\n Informational: no key is looked up from it, and it establishes no trust.", "type": "string" }, "principal_id": { "default": "", - "description": "Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").", + "description": "Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").\n Informational; not read by the verifier.", "type": "string" }, "quota_period": { @@ -57,11 +57,11 @@ "type": "string" }, "revocation_uri": { - "description": "Optional: URI for real-time revocation checking.\n Exchange MAY check this for high-value transactions.\n Not checked for routine low-value access (performance tradeoff).", + "description": "Informational. Revocation lookup is not part of the delegation profile:\n the verifier never fetches this URI. A chain is invalidated early by\n withdrawing the configured root key.", "type": "string" }, "scopes": { - "description": "Scopes granted by this delegation. MUST be a subset of the\n principal's own scopes (attenuation — can only narrow, not widen).", + "description": "Mirror of the terminal token's effective scopes. In the chain, each\n child's scopes are covered by its parent's under the scope-matching rule\n on Requester.scopes (attenuation — can only narrow, not widen).", "items": { "type": "string" }, @@ -69,8 +69,9 @@ }, "token": { "default": null, - "description": "Token bytes. A JWT (base64url-encoded JWS).", - "pattern": "^[A-Za-z0-9+/]*={0,2}$", + "description": "Token bytes. A JWT (base64url-encoded JWS). At most 32768 bytes: the\n complete chain, every embedded parent included, must fit. The limit is a\n wire rule, checked before the token is parsed and independent of whether\n the bytes are a valid JWT; a longer token is an invalid request, not a\n per-item denial.", + "maxLength": 43692, + "pattern": "^[A-Za-z0-9+/]{0,43691}={0,2}$", "type": "string" }, "token_format": { diff --git a/gen/jsonschema/fora.v1.Delegation.schema.strict.json b/gen/jsonschema/fora.v1.Delegation.schema.strict.json index 2b682fff..2af986af 100644 --- a/gen/jsonschema/fora.v1.Delegation.schema.strict.json +++ b/gen/jsonschema/fora.v1.Delegation.schema.strict.json @@ -2,10 +2,10 @@ "$id": "fora.v1.Delegation.schema.strict.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, - "description": "The token is an opaque, signed credential. The format is a JWT (token_format\n \"jwt\"); the field stays open for a future format. Its claims are the\n AUTHORITATIVE source of the grant; the\n plaintext fields below (scopes, expires_at, max_spend_cents, …) are a\n convenience mirror the Exchange MAY use for fast pre-filtering before it\n verifies the token.\n\n Holder binding (the load-bearing property). The grant is bound to a holder key\n via the RFC 7800 `cnf` confirmation claim — `cnf.jkt`, the RFC 7638 JWK\n thumbprint of the holder's key. Verification MUST check that the key signing\n the request (RFC 9421) hashes to that thumbprint; a token possessed without\n the matching private key is rejected. This is what makes a leaked token NOT\n bearer-usable. One exception, the relayed purchase: when a Broker re-packages\n a purchase (BrokerService.ExecuteTransaction), the request an Exchange\n receives is signed by the Broker, not by the holder. There the Exchange checks\n the holder binding against the key each item's AgentAcceptance verifies under\n — the agent's key, resolved from the requester's WBA directory — and not\n against the request signer. A relayed purchase that carries a delegation\n therefore needs an AgentAcceptance on every item; an item without one fails\n the binding (DENIAL_REASON_DELEGATION_INVALID). On a query a Broker\n originated (Broker-led discovery), the Broker signs alone, so a delegation on\n it cannot pass holder binding: the Exchange does not honour it and answers\n as if none were present. How an agent proves a holder-of-key entitlement at\n discovery time through a Broker is an open protocol question. An Exchange\n that requires a verified delegation before it presents a scoped term (see\n LicenseTerm.scopes) therefore presents no such term on that query.\n\n Delegation is a chain of cnf-linked JWTs: a principal narrows a\n grant by issuing a child JWT (cnf = the next holder, scopes ⊆ parent), signed\n by the key the parent's cnf named — the chain-linkage invariant. Verifiers\n need only the root issuer's public key; intermediate keys ride inside the\n chain (JOSE header `jwk`), so verification is offline.\n\n The claim schema is a small registered vocabulary — see the FORA\n delegation-claims profile in the auth spec. All claims are OPTIONAL except the\n holder binding (cnf). Vendors MAY add namespaced claims (their own \"vendor:\"\n namespace; \"fora_\"-prefixed names are reserved for the registered vocabulary\n and MUST NOT be redefined); any constraint a verifier cannot evaluate is\n binding by default (fail closed) unless the issuer marks it advisory —\n mirroring Restriction.advisory.\n\n Scope/time/spend caps are defense-in-depth that bound the blast radius only in\n the residual case where the holder's signing key is also compromised; the\n primary protection against theft is the holder binding above.", + "description": "The token is a signed JWT (token_format \"jwt\"; the field stays open for a\n future format) carrying a holder-bound delegation chain. Its claims are the\n AUTHORITATIVE source of the grant; the plaintext fields below (scopes,\n expires_at, max_spend_cents, …) are a mirror the Exchange MAY use for fast\n pre-filtering before it verifies the token. A mirror establishes nothing: it\n cannot grant a scope, extend validity or raise a cap beyond what the signed\n chain says.\n\n The profile — chain encoding and limits, protected header, claims, clock\n leeway, issuer, audience and holder-binding rules, the verification\n procedure and the outcome of each failure — is the \"Delegation\" section of\n the authentication page and is not restated here; the Go SDK verifier\n (sdk/go/helpers) and the shared chain cases are derived from it. What a\n field reader must know: the grant is bound to a holder key by `cnf.jkt`, and\n the key a request proves possession of MUST hash to the terminal token's\n `cnf.jkt` (the RFC 9421 request signer on a direct call; on a purchase a\n Broker relayed, the key each item's AgentAcceptance verifies under), so a\n token possessed without its private key grants nothing. On a query a Broker\n originated nothing can pass that binding: the Exchange does not honour the\n delegation, answers as if none were present, and presents no term gated\n behind one (see LicenseTerm.scopes).", "properties": { "expires_at": { - "description": "When this delegation expires. Exchange MUST reject expired tokens.", + "description": "Mirror of the chain's earliest `exp`. The Exchange MUST reject an expired\n token; the signed `exp` values decide, not this field.", "format": "date-time", "type": "string" }, @@ -21,7 +21,7 @@ "type": "array" }, "issuer": { - "description": "Token issuer. OIDC issuer URL or GNAP grant server URL.\n Exchange uses this for JWT validation (OIDC discovery → JWKS)\n or GNAP token introspection.", + "description": "Mirror of the ROOT token's `iss`, the identity configured with the trusted\n key (never an intermediate or terminal issuer). Informational: the\n Exchange never discovers a key from it, through OIDC, JWKS or otherwise.", "type": "string" }, "max_accesses": { @@ -44,12 +44,12 @@ }, "principal_domain": { "default": "", - "description": "Who granted this delegation (domain for public key lookup).", + "description": "Mirror of the ROOT token's `iss`, the configured trust anchor's identity.\n Informational: no key is looked up from it, and it establishes no trust.", "type": "string" }, "principal_id": { "default": "", - "description": "Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").", + "description": "Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").\n Informational; not read by the verifier.", "type": "string" }, "quota_period": { @@ -58,11 +58,11 @@ "type": "string" }, "revocation_uri": { - "description": "Optional: URI for real-time revocation checking.\n Exchange MAY check this for high-value transactions.\n Not checked for routine low-value access (performance tradeoff).", + "description": "Informational. Revocation lookup is not part of the delegation profile:\n the verifier never fetches this URI. A chain is invalidated early by\n withdrawing the configured root key.", "type": "string" }, "scopes": { - "description": "Scopes granted by this delegation. MUST be a subset of the\n principal's own scopes (attenuation — can only narrow, not widen).", + "description": "Mirror of the terminal token's effective scopes. In the chain, each\n child's scopes are covered by its parent's under the scope-matching rule\n on Requester.scopes (attenuation — can only narrow, not widen).", "items": { "type": "string" }, @@ -70,8 +70,9 @@ }, "token": { "default": null, - "description": "Token bytes. A JWT (base64url-encoded JWS).", - "pattern": "^[A-Za-z0-9+/]*={0,2}$", + "description": "Token bytes. A JWT (base64url-encoded JWS). At most 32768 bytes: the\n complete chain, every embedded parent included, must fit. The limit is a\n wire rule, checked before the token is parsed and independent of whether\n the bytes are a valid JWT; a longer token is an invalid request, not a\n per-item denial.", + "maxLength": 43692, + "pattern": "^[A-Za-z0-9+/]{0,43691}={0,2}$", "type": "string" }, "token_format": { diff --git a/gen/jsonschema/fora.v1.DiscoveryRequest.schema.json b/gen/jsonschema/fora.v1.DiscoveryRequest.schema.json index fd4014d6..7336d090 100644 --- a/gen/jsonschema/fora.v1.DiscoveryRequest.schema.json +++ b/gen/jsonschema/fora.v1.DiscoveryRequest.schema.json @@ -67,10 +67,10 @@ "type": "object" }, "fora.v1.Delegation": { - "description": "The token is an opaque, signed credential. The format is a JWT (token_format\n \"jwt\"); the field stays open for a future format. Its claims are the\n AUTHORITATIVE source of the grant; the\n plaintext fields below (scopes, expires_at, max_spend_cents, …) are a\n convenience mirror the Exchange MAY use for fast pre-filtering before it\n verifies the token.\n\n Holder binding (the load-bearing property). The grant is bound to a holder key\n via the RFC 7800 `cnf` confirmation claim — `cnf.jkt`, the RFC 7638 JWK\n thumbprint of the holder's key. Verification MUST check that the key signing\n the request (RFC 9421) hashes to that thumbprint; a token possessed without\n the matching private key is rejected. This is what makes a leaked token NOT\n bearer-usable. One exception, the relayed purchase: when a Broker re-packages\n a purchase (BrokerService.ExecuteTransaction), the request an Exchange\n receives is signed by the Broker, not by the holder. There the Exchange checks\n the holder binding against the key each item's AgentAcceptance verifies under\n — the agent's key, resolved from the requester's WBA directory — and not\n against the request signer. A relayed purchase that carries a delegation\n therefore needs an AgentAcceptance on every item; an item without one fails\n the binding (DENIAL_REASON_DELEGATION_INVALID). On a query a Broker\n originated (Broker-led discovery), the Broker signs alone, so a delegation on\n it cannot pass holder binding: the Exchange does not honour it and answers\n as if none were present. How an agent proves a holder-of-key entitlement at\n discovery time through a Broker is an open protocol question. An Exchange\n that requires a verified delegation before it presents a scoped term (see\n LicenseTerm.scopes) therefore presents no such term on that query.\n\n Delegation is a chain of cnf-linked JWTs: a principal narrows a\n grant by issuing a child JWT (cnf = the next holder, scopes ⊆ parent), signed\n by the key the parent's cnf named — the chain-linkage invariant. Verifiers\n need only the root issuer's public key; intermediate keys ride inside the\n chain (JOSE header `jwk`), so verification is offline.\n\n The claim schema is a small registered vocabulary — see the FORA\n delegation-claims profile in the auth spec. All claims are OPTIONAL except the\n holder binding (cnf). Vendors MAY add namespaced claims (their own \"vendor:\"\n namespace; \"fora_\"-prefixed names are reserved for the registered vocabulary\n and MUST NOT be redefined); any constraint a verifier cannot evaluate is\n binding by default (fail closed) unless the issuer marks it advisory —\n mirroring Restriction.advisory.\n\n Scope/time/spend caps are defense-in-depth that bound the blast radius only in\n the residual case where the holder's signing key is also compromised; the\n primary protection against theft is the holder binding above.", + "description": "The token is a signed JWT (token_format \"jwt\"; the field stays open for a\n future format) carrying a holder-bound delegation chain. Its claims are the\n AUTHORITATIVE source of the grant; the plaintext fields below (scopes,\n expires_at, max_spend_cents, …) are a mirror the Exchange MAY use for fast\n pre-filtering before it verifies the token. A mirror establishes nothing: it\n cannot grant a scope, extend validity or raise a cap beyond what the signed\n chain says.\n\n The profile — chain encoding and limits, protected header, claims, clock\n leeway, issuer, audience and holder-binding rules, the verification\n procedure and the outcome of each failure — is the \"Delegation\" section of\n the authentication page and is not restated here; the Go SDK verifier\n (sdk/go/helpers) and the shared chain cases are derived from it. What a\n field reader must know: the grant is bound to a holder key by `cnf.jkt`, and\n the key a request proves possession of MUST hash to the terminal token's\n `cnf.jkt` (the RFC 9421 request signer on a direct call; on a purchase a\n Broker relayed, the key each item's AgentAcceptance verifies under), so a\n token possessed without its private key grants nothing. On a query a Broker\n originated nothing can pass that binding: the Exchange does not honour the\n delegation, answers as if none were present, and presents no term gated\n behind one (see LicenseTerm.scopes).", "properties": { "expires_at": { - "description": "When this delegation expires. Exchange MUST reject expired tokens.", + "description": "Mirror of the chain's earliest `exp`. The Exchange MUST reject an expired\n token; the signed `exp` values decide, not this field.", "format": "date-time", "type": "string" }, @@ -86,7 +86,7 @@ "type": "array" }, "issuer": { - "description": "Token issuer. OIDC issuer URL or GNAP grant server URL.\n Exchange uses this for JWT validation (OIDC discovery → JWKS)\n or GNAP token introspection.", + "description": "Mirror of the ROOT token's `iss`, the identity configured with the trusted\n key (never an intermediate or terminal issuer). Informational: the\n Exchange never discovers a key from it, through OIDC, JWKS or otherwise.", "type": "string" }, "max_accesses": { @@ -109,12 +109,12 @@ }, "principal_domain": { "default": "", - "description": "Who granted this delegation (domain for public key lookup).", + "description": "Mirror of the ROOT token's `iss`, the configured trust anchor's identity.\n Informational: no key is looked up from it, and it establishes no trust.", "type": "string" }, "principal_id": { "default": "", - "description": "Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").", + "description": "Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").\n Informational; not read by the verifier.", "type": "string" }, "quota_period": { @@ -123,11 +123,11 @@ "type": "string" }, "revocation_uri": { - "description": "Optional: URI for real-time revocation checking.\n Exchange MAY check this for high-value transactions.\n Not checked for routine low-value access (performance tradeoff).", + "description": "Informational. Revocation lookup is not part of the delegation profile:\n the verifier never fetches this URI. A chain is invalidated early by\n withdrawing the configured root key.", "type": "string" }, "scopes": { - "description": "Scopes granted by this delegation. MUST be a subset of the\n principal's own scopes (attenuation — can only narrow, not widen).", + "description": "Mirror of the terminal token's effective scopes. In the chain, each\n child's scopes are covered by its parent's under the scope-matching rule\n on Requester.scopes (attenuation — can only narrow, not widen).", "items": { "type": "string" }, @@ -135,8 +135,9 @@ }, "token": { "default": null, - "description": "Token bytes. A JWT (base64url-encoded JWS).", - "pattern": "^[A-Za-z0-9+/]*={0,2}$", + "description": "Token bytes. A JWT (base64url-encoded JWS). At most 32768 bytes: the\n complete chain, every embedded parent included, must fit. The limit is a\n wire rule, checked before the token is parsed and independent of whether\n the bytes are a valid JWT; a longer token is an invalid request, not a\n per-item denial.", + "maxLength": 43692, + "pattern": "^[A-Za-z0-9+/]{0,43691}={0,2}$", "type": "string" }, "token_format": { @@ -271,7 +272,7 @@ "type": "string" }, "scopes": { - "description": "EXISTENCE HIDING. A requester never learns about a resource outside its\n scopes. When the scopes leave no presentable offer for a resource, the\n Exchange answers with no offers and no absence reason (an OfferGroup with\n empty `offers` and `absence_reason` unset), exactly as for any resource\n with nothing to offer. A Broker relaying that answer adds no reason of its\n own. At purchase, an offer the Exchange presented is honoured until it\n expires, so a scope refusal cannot arise there. No reason in the protocol\n reports a scope shortfall: OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT and\n DENIAL_REASON_SCOPE_INSUFFICIENT are deprecated and never sent.\n\n Scope format: colon-separated segments, \"{domain}:{permission}\" or\n \"{profile}:{permission}\", optionally multi-segment (\"dist:US:CA\");\n matching is segment-wise per the rule below (no implicit hierarchy).\n Examples:\n \"credit:read\" — can access credit reports\n \"subscription:marketdata-2026\" — has active MarketData subscription\n \"academic:*\" — full access to academic resources\n \"internal:reports\" — can access internal reports\n \"*\" — unrestricted (public Exchange default)\n\n Matching is SEGMENT-WISE (\":\" separated). A granted scope G covers a\n required scope R iff, segment by segment, each G segment equals the\n corresponding R segment or is \"*\"; a terminal \"*\" matches all remaining\n segments. There is NO implicit prefix match, and a grant NARROWER than\n the requirement does not cover it (G must be equal-to-or-broader than R).\n Examples: \"dist:*\" covers \"dist:US\" and \"dist:US:CA\"; \"dist:US:*\" covers\n \"dist:US:CA\" but not \"dist:EU\"; bare \"dist\" covers only \"dist\"; granted\n \"dist:US:CA\" does NOT cover required \"dist:US\"; \"*\" covers everything.\n This same rule governs LicenseTerm.scopes — one algorithm protocol-wide.\n\n When empty, Exchange applies its default access policy (typically\n returns all publicly available resources).", + "description": "EXISTENCE HIDING. A requester never learns about a resource outside its\n scopes. When the scopes leave no presentable offer for a resource, the\n Exchange answers with no offers and no absence reason (an OfferGroup with\n empty `offers` and `absence_reason` unset), exactly as for any resource\n with nothing to offer. A Broker relaying that answer adds no reason of its\n own. At purchase, an offer the Exchange presented is honoured until it\n expires, so a scope refusal cannot arise there. No reason in the protocol\n reports a scope shortfall: OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT and\n DENIAL_REASON_SCOPE_INSUFFICIENT are deprecated and never sent.\n\n Scope format: colon-separated segments, \"{domain}:{permission}\" or\n \"{profile}:{permission}\", optionally multi-segment (\"dist:US:CA\");\n matching is segment-wise per the rule below (no implicit hierarchy).\n Examples:\n \"credit:read\" — can access credit reports\n \"subscription:marketdata-2026\" — has active MarketData subscription\n \"academic:*\" — full access to academic resources\n \"internal:reports\" — can access internal reports\n \"*\" — unrestricted (public Exchange default)\n\n Matching is SEGMENT-WISE (\":\" separated). A granted scope G covers a\n required scope R iff, segment by segment, each G segment equals the\n corresponding R segment or is \"*\"; a terminal \"*\" matches all remaining\n segments, of which there must be at least one. There is NO implicit prefix\n match, and a grant NARROWER than the requirement does not cover it (G must\n be equal-to-or-broader than R), so a grant with more segments than the\n requirement never covers it, even when its last segment is \"*\". A \"*\" in R\n is a literal segment: a narrow grant never covers a wildcard requirement.\n Matching is case-sensitive and never trims. Examples: \"dist:*\" covers\n \"dist:US\", \"dist:US:CA\" and \"dist:US:*\" but not \"dist\"; \"dist:US:*\" covers\n \"dist:US:CA\" but not \"dist:EU\" or \"dist:US\"; \"publisher:*:read\" covers\n \"publisher:news:read\" but not \"publisher:news:*\"; bare \"dist\" covers only\n \"dist\"; granted \"dist:US:CA\" does NOT cover required \"dist:US\"; \"*\" covers\n everything. This same rule governs LicenseTerm.scopes and the attenuation\n of a delegation child against its parent — one algorithm protocol-wide.\n\n When empty, Exchange applies its default access policy (typically\n returns all publicly available resources).", "items": { "type": "string" }, diff --git a/gen/jsonschema/fora.v1.DiscoveryRequest.schema.strict.json b/gen/jsonschema/fora.v1.DiscoveryRequest.schema.strict.json index 5388a992..48ff1026 100644 --- a/gen/jsonschema/fora.v1.DiscoveryRequest.schema.strict.json +++ b/gen/jsonschema/fora.v1.DiscoveryRequest.schema.strict.json @@ -70,10 +70,10 @@ }, "fora.v1.Delegation": { "additionalProperties": false, - "description": "The token is an opaque, signed credential. The format is a JWT (token_format\n \"jwt\"); the field stays open for a future format. Its claims are the\n AUTHORITATIVE source of the grant; the\n plaintext fields below (scopes, expires_at, max_spend_cents, …) are a\n convenience mirror the Exchange MAY use for fast pre-filtering before it\n verifies the token.\n\n Holder binding (the load-bearing property). The grant is bound to a holder key\n via the RFC 7800 `cnf` confirmation claim — `cnf.jkt`, the RFC 7638 JWK\n thumbprint of the holder's key. Verification MUST check that the key signing\n the request (RFC 9421) hashes to that thumbprint; a token possessed without\n the matching private key is rejected. This is what makes a leaked token NOT\n bearer-usable. One exception, the relayed purchase: when a Broker re-packages\n a purchase (BrokerService.ExecuteTransaction), the request an Exchange\n receives is signed by the Broker, not by the holder. There the Exchange checks\n the holder binding against the key each item's AgentAcceptance verifies under\n — the agent's key, resolved from the requester's WBA directory — and not\n against the request signer. A relayed purchase that carries a delegation\n therefore needs an AgentAcceptance on every item; an item without one fails\n the binding (DENIAL_REASON_DELEGATION_INVALID). On a query a Broker\n originated (Broker-led discovery), the Broker signs alone, so a delegation on\n it cannot pass holder binding: the Exchange does not honour it and answers\n as if none were present. How an agent proves a holder-of-key entitlement at\n discovery time through a Broker is an open protocol question. An Exchange\n that requires a verified delegation before it presents a scoped term (see\n LicenseTerm.scopes) therefore presents no such term on that query.\n\n Delegation is a chain of cnf-linked JWTs: a principal narrows a\n grant by issuing a child JWT (cnf = the next holder, scopes ⊆ parent), signed\n by the key the parent's cnf named — the chain-linkage invariant. Verifiers\n need only the root issuer's public key; intermediate keys ride inside the\n chain (JOSE header `jwk`), so verification is offline.\n\n The claim schema is a small registered vocabulary — see the FORA\n delegation-claims profile in the auth spec. All claims are OPTIONAL except the\n holder binding (cnf). Vendors MAY add namespaced claims (their own \"vendor:\"\n namespace; \"fora_\"-prefixed names are reserved for the registered vocabulary\n and MUST NOT be redefined); any constraint a verifier cannot evaluate is\n binding by default (fail closed) unless the issuer marks it advisory —\n mirroring Restriction.advisory.\n\n Scope/time/spend caps are defense-in-depth that bound the blast radius only in\n the residual case where the holder's signing key is also compromised; the\n primary protection against theft is the holder binding above.", + "description": "The token is a signed JWT (token_format \"jwt\"; the field stays open for a\n future format) carrying a holder-bound delegation chain. Its claims are the\n AUTHORITATIVE source of the grant; the plaintext fields below (scopes,\n expires_at, max_spend_cents, …) are a mirror the Exchange MAY use for fast\n pre-filtering before it verifies the token. A mirror establishes nothing: it\n cannot grant a scope, extend validity or raise a cap beyond what the signed\n chain says.\n\n The profile — chain encoding and limits, protected header, claims, clock\n leeway, issuer, audience and holder-binding rules, the verification\n procedure and the outcome of each failure — is the \"Delegation\" section of\n the authentication page and is not restated here; the Go SDK verifier\n (sdk/go/helpers) and the shared chain cases are derived from it. What a\n field reader must know: the grant is bound to a holder key by `cnf.jkt`, and\n the key a request proves possession of MUST hash to the terminal token's\n `cnf.jkt` (the RFC 9421 request signer on a direct call; on a purchase a\n Broker relayed, the key each item's AgentAcceptance verifies under), so a\n token possessed without its private key grants nothing. On a query a Broker\n originated nothing can pass that binding: the Exchange does not honour the\n delegation, answers as if none were present, and presents no term gated\n behind one (see LicenseTerm.scopes).", "properties": { "expires_at": { - "description": "When this delegation expires. Exchange MUST reject expired tokens.", + "description": "Mirror of the chain's earliest `exp`. The Exchange MUST reject an expired\n token; the signed `exp` values decide, not this field.", "format": "date-time", "type": "string" }, @@ -89,7 +89,7 @@ "type": "array" }, "issuer": { - "description": "Token issuer. OIDC issuer URL or GNAP grant server URL.\n Exchange uses this for JWT validation (OIDC discovery → JWKS)\n or GNAP token introspection.", + "description": "Mirror of the ROOT token's `iss`, the identity configured with the trusted\n key (never an intermediate or terminal issuer). Informational: the\n Exchange never discovers a key from it, through OIDC, JWKS or otherwise.", "type": "string" }, "max_accesses": { @@ -112,12 +112,12 @@ }, "principal_domain": { "default": "", - "description": "Who granted this delegation (domain for public key lookup).", + "description": "Mirror of the ROOT token's `iss`, the configured trust anchor's identity.\n Informational: no key is looked up from it, and it establishes no trust.", "type": "string" }, "principal_id": { "default": "", - "description": "Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").", + "description": "Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").\n Informational; not read by the verifier.", "type": "string" }, "quota_period": { @@ -126,11 +126,11 @@ "type": "string" }, "revocation_uri": { - "description": "Optional: URI for real-time revocation checking.\n Exchange MAY check this for high-value transactions.\n Not checked for routine low-value access (performance tradeoff).", + "description": "Informational. Revocation lookup is not part of the delegation profile:\n the verifier never fetches this URI. A chain is invalidated early by\n withdrawing the configured root key.", "type": "string" }, "scopes": { - "description": "Scopes granted by this delegation. MUST be a subset of the\n principal's own scopes (attenuation — can only narrow, not widen).", + "description": "Mirror of the terminal token's effective scopes. In the chain, each\n child's scopes are covered by its parent's under the scope-matching rule\n on Requester.scopes (attenuation — can only narrow, not widen).", "items": { "type": "string" }, @@ -138,8 +138,9 @@ }, "token": { "default": null, - "description": "Token bytes. A JWT (base64url-encoded JWS).", - "pattern": "^[A-Za-z0-9+/]*={0,2}$", + "description": "Token bytes. A JWT (base64url-encoded JWS). At most 32768 bytes: the\n complete chain, every embedded parent included, must fit. The limit is a\n wire rule, checked before the token is parsed and independent of whether\n the bytes are a valid JWT; a longer token is an invalid request, not a\n per-item denial.", + "maxLength": 43692, + "pattern": "^[A-Za-z0-9+/]{0,43691}={0,2}$", "type": "string" }, "token_format": { @@ -276,7 +277,7 @@ "type": "string" }, "scopes": { - "description": "EXISTENCE HIDING. A requester never learns about a resource outside its\n scopes. When the scopes leave no presentable offer for a resource, the\n Exchange answers with no offers and no absence reason (an OfferGroup with\n empty `offers` and `absence_reason` unset), exactly as for any resource\n with nothing to offer. A Broker relaying that answer adds no reason of its\n own. At purchase, an offer the Exchange presented is honoured until it\n expires, so a scope refusal cannot arise there. No reason in the protocol\n reports a scope shortfall: OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT and\n DENIAL_REASON_SCOPE_INSUFFICIENT are deprecated and never sent.\n\n Scope format: colon-separated segments, \"{domain}:{permission}\" or\n \"{profile}:{permission}\", optionally multi-segment (\"dist:US:CA\");\n matching is segment-wise per the rule below (no implicit hierarchy).\n Examples:\n \"credit:read\" — can access credit reports\n \"subscription:marketdata-2026\" — has active MarketData subscription\n \"academic:*\" — full access to academic resources\n \"internal:reports\" — can access internal reports\n \"*\" — unrestricted (public Exchange default)\n\n Matching is SEGMENT-WISE (\":\" separated). A granted scope G covers a\n required scope R iff, segment by segment, each G segment equals the\n corresponding R segment or is \"*\"; a terminal \"*\" matches all remaining\n segments. There is NO implicit prefix match, and a grant NARROWER than\n the requirement does not cover it (G must be equal-to-or-broader than R).\n Examples: \"dist:*\" covers \"dist:US\" and \"dist:US:CA\"; \"dist:US:*\" covers\n \"dist:US:CA\" but not \"dist:EU\"; bare \"dist\" covers only \"dist\"; granted\n \"dist:US:CA\" does NOT cover required \"dist:US\"; \"*\" covers everything.\n This same rule governs LicenseTerm.scopes — one algorithm protocol-wide.\n\n When empty, Exchange applies its default access policy (typically\n returns all publicly available resources).", + "description": "EXISTENCE HIDING. A requester never learns about a resource outside its\n scopes. When the scopes leave no presentable offer for a resource, the\n Exchange answers with no offers and no absence reason (an OfferGroup with\n empty `offers` and `absence_reason` unset), exactly as for any resource\n with nothing to offer. A Broker relaying that answer adds no reason of its\n own. At purchase, an offer the Exchange presented is honoured until it\n expires, so a scope refusal cannot arise there. No reason in the protocol\n reports a scope shortfall: OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT and\n DENIAL_REASON_SCOPE_INSUFFICIENT are deprecated and never sent.\n\n Scope format: colon-separated segments, \"{domain}:{permission}\" or\n \"{profile}:{permission}\", optionally multi-segment (\"dist:US:CA\");\n matching is segment-wise per the rule below (no implicit hierarchy).\n Examples:\n \"credit:read\" — can access credit reports\n \"subscription:marketdata-2026\" — has active MarketData subscription\n \"academic:*\" — full access to academic resources\n \"internal:reports\" — can access internal reports\n \"*\" — unrestricted (public Exchange default)\n\n Matching is SEGMENT-WISE (\":\" separated). A granted scope G covers a\n required scope R iff, segment by segment, each G segment equals the\n corresponding R segment or is \"*\"; a terminal \"*\" matches all remaining\n segments, of which there must be at least one. There is NO implicit prefix\n match, and a grant NARROWER than the requirement does not cover it (G must\n be equal-to-or-broader than R), so a grant with more segments than the\n requirement never covers it, even when its last segment is \"*\". A \"*\" in R\n is a literal segment: a narrow grant never covers a wildcard requirement.\n Matching is case-sensitive and never trims. Examples: \"dist:*\" covers\n \"dist:US\", \"dist:US:CA\" and \"dist:US:*\" but not \"dist\"; \"dist:US:*\" covers\n \"dist:US:CA\" but not \"dist:EU\" or \"dist:US\"; \"publisher:*:read\" covers\n \"publisher:news:read\" but not \"publisher:news:*\"; bare \"dist\" covers only\n \"dist\"; granted \"dist:US:CA\" does NOT cover required \"dist:US\"; \"*\" covers\n everything. This same rule governs LicenseTerm.scopes and the attenuation\n of a delegation child against its parent — one algorithm protocol-wide.\n\n When empty, Exchange applies its default access policy (typically\n returns all publicly available resources).", "items": { "type": "string" }, diff --git a/gen/jsonschema/fora.v1.DiscoveryResponse.schema.json b/gen/jsonschema/fora.v1.DiscoveryResponse.schema.json index 657bbe4c..a281f799 100644 --- a/gen/jsonschema/fora.v1.DiscoveryResponse.schema.json +++ b/gen/jsonschema/fora.v1.DiscoveryResponse.schema.json @@ -71,7 +71,7 @@ "type": "array" }, "scopes": { - "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n also require a verified delegation that grants these scopes before it\n presents the term. To a requester that presents none, it answers as if the\n term did not exist, as existence hiding requires (see Requester.scopes). An\n offer the Exchange presented is honoured until it expires.\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", + "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n instead gate a term behind a verified delegation (a subscription term is\n the usual case): then only the delegation chain's verified effective scopes\n are matched against these scopes, and Requester.scopes is ignored for that\n term. To a requester that presents no valid delegation, it answers as if\n the term did not exist, as existence hiding requires (see\n Requester.scopes). An offer the Exchange presented is honoured until it\n expires, and a gated offer expires no later than the chain's effective\n expiry (see Delegation).\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments (at least one), and there is NO implicit prefix match\n (a grant narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", "items": { "type": "string" }, @@ -213,7 +213,7 @@ "type": "string" }, "expires_at": { - "description": "When this offer expires (ISO 8601).", + "description": "When this offer expires (ISO 8601). An offer for a term gated behind a\n verified delegation MUST NOT expire later than that delegation chain's\n effective expiry, the earliest `exp` in the chain (see Delegation).", "format": "date-time", "type": "string" }, diff --git a/gen/jsonschema/fora.v1.DiscoveryResponse.schema.strict.json b/gen/jsonschema/fora.v1.DiscoveryResponse.schema.strict.json index 40d14188..b0f2b668 100644 --- a/gen/jsonschema/fora.v1.DiscoveryResponse.schema.strict.json +++ b/gen/jsonschema/fora.v1.DiscoveryResponse.schema.strict.json @@ -73,7 +73,7 @@ "type": "array" }, "scopes": { - "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n also require a verified delegation that grants these scopes before it\n presents the term. To a requester that presents none, it answers as if the\n term did not exist, as existence hiding requires (see Requester.scopes). An\n offer the Exchange presented is honoured until it expires.\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", + "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n instead gate a term behind a verified delegation (a subscription term is\n the usual case): then only the delegation chain's verified effective scopes\n are matched against these scopes, and Requester.scopes is ignored for that\n term. To a requester that presents no valid delegation, it answers as if\n the term did not exist, as existence hiding requires (see\n Requester.scopes). An offer the Exchange presented is honoured until it\n expires, and a gated offer expires no later than the chain's effective\n expiry (see Delegation).\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments (at least one), and there is NO implicit prefix match\n (a grant narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", "items": { "type": "string" }, @@ -217,7 +217,7 @@ "type": "string" }, "expires_at": { - "description": "When this offer expires (ISO 8601).", + "description": "When this offer expires (ISO 8601). An offer for a term gated behind a\n verified delegation MUST NOT expire later than that delegation chain's\n effective expiry, the earliest `exp` in the chain (see Delegation).", "format": "date-time", "type": "string" }, diff --git a/gen/jsonschema/fora.v1.LicenseTerm.schema.json b/gen/jsonschema/fora.v1.LicenseTerm.schema.json index ba9b1bc2..3880601e 100644 --- a/gen/jsonschema/fora.v1.LicenseTerm.schema.json +++ b/gen/jsonschema/fora.v1.LicenseTerm.schema.json @@ -329,7 +329,7 @@ "type": "array" }, "scopes": { - "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n also require a verified delegation that grants these scopes before it\n presents the term. To a requester that presents none, it answers as if the\n term did not exist, as existence hiding requires (see Requester.scopes). An\n offer the Exchange presented is honoured until it expires.\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", + "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n instead gate a term behind a verified delegation (a subscription term is\n the usual case): then only the delegation chain's verified effective scopes\n are matched against these scopes, and Requester.scopes is ignored for that\n term. To a requester that presents no valid delegation, it answers as if\n the term did not exist, as existence hiding requires (see\n Requester.scopes). An offer the Exchange presented is honoured until it\n expires, and a gated offer expires no later than the chain's effective\n expiry (see Delegation).\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments (at least one), and there is NO implicit prefix match\n (a grant narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", "items": { "type": "string" }, diff --git a/gen/jsonschema/fora.v1.LicenseTerm.schema.strict.json b/gen/jsonschema/fora.v1.LicenseTerm.schema.strict.json index bdba9171..2e4b1b04 100644 --- a/gen/jsonschema/fora.v1.LicenseTerm.schema.strict.json +++ b/gen/jsonschema/fora.v1.LicenseTerm.schema.strict.json @@ -335,7 +335,7 @@ "type": "array" }, "scopes": { - "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n also require a verified delegation that grants these scopes before it\n presents the term. To a requester that presents none, it answers as if the\n term did not exist, as existence hiding requires (see Requester.scopes). An\n offer the Exchange presented is honoured until it expires.\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", + "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n instead gate a term behind a verified delegation (a subscription term is\n the usual case): then only the delegation chain's verified effective scopes\n are matched against these scopes, and Requester.scopes is ignored for that\n term. To a requester that presents no valid delegation, it answers as if\n the term did not exist, as existence hiding requires (see\n Requester.scopes). An offer the Exchange presented is honoured until it\n expires, and a gated offer expires no later than the chain's effective\n expiry (see Delegation).\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments (at least one), and there is NO implicit prefix match\n (a grant narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", "items": { "type": "string" }, diff --git a/gen/jsonschema/fora.v1.Offer.schema.json b/gen/jsonschema/fora.v1.Offer.schema.json index b8c45940..f0d30c4b 100644 --- a/gen/jsonschema/fora.v1.Offer.schema.json +++ b/gen/jsonschema/fora.v1.Offer.schema.json @@ -71,7 +71,7 @@ "type": "array" }, "scopes": { - "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n also require a verified delegation that grants these scopes before it\n presents the term. To a requester that presents none, it answers as if the\n term did not exist, as existence hiding requires (see Requester.scopes). An\n offer the Exchange presented is honoured until it expires.\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", + "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n instead gate a term behind a verified delegation (a subscription term is\n the usual case): then only the delegation chain's verified effective scopes\n are matched against these scopes, and Requester.scopes is ignored for that\n term. To a requester that presents no valid delegation, it answers as if\n the term did not exist, as existence hiding requires (see\n Requester.scopes). An offer the Exchange presented is honoured until it\n expires, and a gated offer expires no later than the chain's effective\n expiry (see Delegation).\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments (at least one), and there is NO implicit prefix match\n (a grant narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", "items": { "type": "string" }, @@ -666,7 +666,7 @@ "type": "string" }, "expires_at": { - "description": "When this offer expires (ISO 8601).", + "description": "When this offer expires (ISO 8601). An offer for a term gated behind a\n verified delegation MUST NOT expire later than that delegation chain's\n effective expiry, the earliest `exp` in the chain (see Delegation).", "format": "date-time", "type": "string" }, diff --git a/gen/jsonschema/fora.v1.Offer.schema.strict.json b/gen/jsonschema/fora.v1.Offer.schema.strict.json index 1b5749bc..ef7eb6b6 100644 --- a/gen/jsonschema/fora.v1.Offer.schema.strict.json +++ b/gen/jsonschema/fora.v1.Offer.schema.strict.json @@ -73,7 +73,7 @@ "type": "array" }, "scopes": { - "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n also require a verified delegation that grants these scopes before it\n presents the term. To a requester that presents none, it answers as if the\n term did not exist, as existence hiding requires (see Requester.scopes). An\n offer the Exchange presented is honoured until it expires.\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", + "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n instead gate a term behind a verified delegation (a subscription term is\n the usual case): then only the delegation chain's verified effective scopes\n are matched against these scopes, and Requester.scopes is ignored for that\n term. To a requester that presents no valid delegation, it answers as if\n the term did not exist, as existence hiding requires (see\n Requester.scopes). An offer the Exchange presented is honoured until it\n expires, and a gated offer expires no later than the chain's effective\n expiry (see Delegation).\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments (at least one), and there is NO implicit prefix match\n (a grant narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", "items": { "type": "string" }, @@ -678,7 +678,7 @@ "type": "string" }, "expires_at": { - "description": "When this offer expires (ISO 8601).", + "description": "When this offer expires (ISO 8601). An offer for a term gated behind a\n verified delegation MUST NOT expire later than that delegation chain's\n effective expiry, the earliest `exp` in the chain (see Delegation).", "format": "date-time", "type": "string" }, diff --git a/gen/jsonschema/fora.v1.OfferGroup.schema.json b/gen/jsonschema/fora.v1.OfferGroup.schema.json index 166dc995..b1e201cc 100644 --- a/gen/jsonschema/fora.v1.OfferGroup.schema.json +++ b/gen/jsonschema/fora.v1.OfferGroup.schema.json @@ -71,7 +71,7 @@ "type": "array" }, "scopes": { - "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n also require a verified delegation that grants these scopes before it\n presents the term. To a requester that presents none, it answers as if the\n term did not exist, as existence hiding requires (see Requester.scopes). An\n offer the Exchange presented is honoured until it expires.\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", + "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n instead gate a term behind a verified delegation (a subscription term is\n the usual case): then only the delegation chain's verified effective scopes\n are matched against these scopes, and Requester.scopes is ignored for that\n term. To a requester that presents no valid delegation, it answers as if\n the term did not exist, as existence hiding requires (see\n Requester.scopes). An offer the Exchange presented is honoured until it\n expires, and a gated offer expires no later than the chain's effective\n expiry (see Delegation).\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments (at least one), and there is NO implicit prefix match\n (a grant narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", "items": { "type": "string" }, @@ -213,7 +213,7 @@ "type": "string" }, "expires_at": { - "description": "When this offer expires (ISO 8601).", + "description": "When this offer expires (ISO 8601). An offer for a term gated behind a\n verified delegation MUST NOT expire later than that delegation chain's\n effective expiry, the earliest `exp` in the chain (see Delegation).", "format": "date-time", "type": "string" }, diff --git a/gen/jsonschema/fora.v1.OfferGroup.schema.strict.json b/gen/jsonschema/fora.v1.OfferGroup.schema.strict.json index 3d079315..0c54e43f 100644 --- a/gen/jsonschema/fora.v1.OfferGroup.schema.strict.json +++ b/gen/jsonschema/fora.v1.OfferGroup.schema.strict.json @@ -73,7 +73,7 @@ "type": "array" }, "scopes": { - "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n also require a verified delegation that grants these scopes before it\n presents the term. To a requester that presents none, it answers as if the\n term did not exist, as existence hiding requires (see Requester.scopes). An\n offer the Exchange presented is honoured until it expires.\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", + "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n instead gate a term behind a verified delegation (a subscription term is\n the usual case): then only the delegation chain's verified effective scopes\n are matched against these scopes, and Requester.scopes is ignored for that\n term. To a requester that presents no valid delegation, it answers as if\n the term did not exist, as existence hiding requires (see\n Requester.scopes). An offer the Exchange presented is honoured until it\n expires, and a gated offer expires no later than the chain's effective\n expiry (see Delegation).\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments (at least one), and there is NO implicit prefix match\n (a grant narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", "items": { "type": "string" }, @@ -217,7 +217,7 @@ "type": "string" }, "expires_at": { - "description": "When this offer expires (ISO 8601).", + "description": "When this offer expires (ISO 8601). An offer for a term gated behind a\n verified delegation MUST NOT expire later than that delegation chain's\n effective expiry, the earliest `exp` in the chain (see Delegation).", "format": "date-time", "type": "string" }, diff --git a/gen/jsonschema/fora.v1.PushResourcesRequest.schema.json b/gen/jsonschema/fora.v1.PushResourcesRequest.schema.json index 0950deca..94048470 100644 --- a/gen/jsonschema/fora.v1.PushResourcesRequest.schema.json +++ b/gen/jsonschema/fora.v1.PushResourcesRequest.schema.json @@ -71,7 +71,7 @@ "type": "array" }, "scopes": { - "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n also require a verified delegation that grants these scopes before it\n presents the term. To a requester that presents none, it answers as if the\n term did not exist, as existence hiding requires (see Requester.scopes). An\n offer the Exchange presented is honoured until it expires.\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", + "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n instead gate a term behind a verified delegation (a subscription term is\n the usual case): then only the delegation chain's verified effective scopes\n are matched against these scopes, and Requester.scopes is ignored for that\n term. To a requester that presents no valid delegation, it answers as if\n the term did not exist, as existence hiding requires (see\n Requester.scopes). An offer the Exchange presented is honoured until it\n expires, and a gated offer expires no later than the chain's effective\n expiry (see Delegation).\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments (at least one), and there is NO implicit prefix match\n (a grant narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", "items": { "type": "string" }, diff --git a/gen/jsonschema/fora.v1.PushResourcesRequest.schema.strict.json b/gen/jsonschema/fora.v1.PushResourcesRequest.schema.strict.json index 1b0c97fa..c0914e67 100644 --- a/gen/jsonschema/fora.v1.PushResourcesRequest.schema.strict.json +++ b/gen/jsonschema/fora.v1.PushResourcesRequest.schema.strict.json @@ -73,7 +73,7 @@ "type": "array" }, "scopes": { - "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n also require a verified delegation that grants these scopes before it\n presents the term. To a requester that presents none, it answers as if the\n term did not exist, as existence hiding requires (see Requester.scopes). An\n offer the Exchange presented is honoured until it expires.\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", + "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n instead gate a term behind a verified delegation (a subscription term is\n the usual case): then only the delegation chain's verified effective scopes\n are matched against these scopes, and Requester.scopes is ignored for that\n term. To a requester that presents no valid delegation, it answers as if\n the term did not exist, as existence hiding requires (see\n Requester.scopes). An offer the Exchange presented is honoured until it\n expires, and a gated offer expires no later than the chain's effective\n expiry (see Delegation).\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments (at least one), and there is NO implicit prefix match\n (a grant narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", "items": { "type": "string" }, diff --git a/gen/jsonschema/fora.v1.Requester.schema.json b/gen/jsonschema/fora.v1.Requester.schema.json index a035d9c1..3382cafd 100644 --- a/gen/jsonschema/fora.v1.Requester.schema.json +++ b/gen/jsonschema/fora.v1.Requester.schema.json @@ -1,10 +1,10 @@ { "$defs": { "fora.v1.Delegation": { - "description": "The token is an opaque, signed credential. The format is a JWT (token_format\n \"jwt\"); the field stays open for a future format. Its claims are the\n AUTHORITATIVE source of the grant; the\n plaintext fields below (scopes, expires_at, max_spend_cents, …) are a\n convenience mirror the Exchange MAY use for fast pre-filtering before it\n verifies the token.\n\n Holder binding (the load-bearing property). The grant is bound to a holder key\n via the RFC 7800 `cnf` confirmation claim — `cnf.jkt`, the RFC 7638 JWK\n thumbprint of the holder's key. Verification MUST check that the key signing\n the request (RFC 9421) hashes to that thumbprint; a token possessed without\n the matching private key is rejected. This is what makes a leaked token NOT\n bearer-usable. One exception, the relayed purchase: when a Broker re-packages\n a purchase (BrokerService.ExecuteTransaction), the request an Exchange\n receives is signed by the Broker, not by the holder. There the Exchange checks\n the holder binding against the key each item's AgentAcceptance verifies under\n — the agent's key, resolved from the requester's WBA directory — and not\n against the request signer. A relayed purchase that carries a delegation\n therefore needs an AgentAcceptance on every item; an item without one fails\n the binding (DENIAL_REASON_DELEGATION_INVALID). On a query a Broker\n originated (Broker-led discovery), the Broker signs alone, so a delegation on\n it cannot pass holder binding: the Exchange does not honour it and answers\n as if none were present. How an agent proves a holder-of-key entitlement at\n discovery time through a Broker is an open protocol question. An Exchange\n that requires a verified delegation before it presents a scoped term (see\n LicenseTerm.scopes) therefore presents no such term on that query.\n\n Delegation is a chain of cnf-linked JWTs: a principal narrows a\n grant by issuing a child JWT (cnf = the next holder, scopes ⊆ parent), signed\n by the key the parent's cnf named — the chain-linkage invariant. Verifiers\n need only the root issuer's public key; intermediate keys ride inside the\n chain (JOSE header `jwk`), so verification is offline.\n\n The claim schema is a small registered vocabulary — see the FORA\n delegation-claims profile in the auth spec. All claims are OPTIONAL except the\n holder binding (cnf). Vendors MAY add namespaced claims (their own \"vendor:\"\n namespace; \"fora_\"-prefixed names are reserved for the registered vocabulary\n and MUST NOT be redefined); any constraint a verifier cannot evaluate is\n binding by default (fail closed) unless the issuer marks it advisory —\n mirroring Restriction.advisory.\n\n Scope/time/spend caps are defense-in-depth that bound the blast radius only in\n the residual case where the holder's signing key is also compromised; the\n primary protection against theft is the holder binding above.", + "description": "The token is a signed JWT (token_format \"jwt\"; the field stays open for a\n future format) carrying a holder-bound delegation chain. Its claims are the\n AUTHORITATIVE source of the grant; the plaintext fields below (scopes,\n expires_at, max_spend_cents, …) are a mirror the Exchange MAY use for fast\n pre-filtering before it verifies the token. A mirror establishes nothing: it\n cannot grant a scope, extend validity or raise a cap beyond what the signed\n chain says.\n\n The profile — chain encoding and limits, protected header, claims, clock\n leeway, issuer, audience and holder-binding rules, the verification\n procedure and the outcome of each failure — is the \"Delegation\" section of\n the authentication page and is not restated here; the Go SDK verifier\n (sdk/go/helpers) and the shared chain cases are derived from it. What a\n field reader must know: the grant is bound to a holder key by `cnf.jkt`, and\n the key a request proves possession of MUST hash to the terminal token's\n `cnf.jkt` (the RFC 9421 request signer on a direct call; on a purchase a\n Broker relayed, the key each item's AgentAcceptance verifies under), so a\n token possessed without its private key grants nothing. On a query a Broker\n originated nothing can pass that binding: the Exchange does not honour the\n delegation, answers as if none were present, and presents no term gated\n behind one (see LicenseTerm.scopes).", "properties": { "expires_at": { - "description": "When this delegation expires. Exchange MUST reject expired tokens.", + "description": "Mirror of the chain's earliest `exp`. The Exchange MUST reject an expired\n token; the signed `exp` values decide, not this field.", "format": "date-time", "type": "string" }, @@ -20,7 +20,7 @@ "type": "array" }, "issuer": { - "description": "Token issuer. OIDC issuer URL or GNAP grant server URL.\n Exchange uses this for JWT validation (OIDC discovery → JWKS)\n or GNAP token introspection.", + "description": "Mirror of the ROOT token's `iss`, the identity configured with the trusted\n key (never an intermediate or terminal issuer). Informational: the\n Exchange never discovers a key from it, through OIDC, JWKS or otherwise.", "type": "string" }, "max_accesses": { @@ -43,12 +43,12 @@ }, "principal_domain": { "default": "", - "description": "Who granted this delegation (domain for public key lookup).", + "description": "Mirror of the ROOT token's `iss`, the configured trust anchor's identity.\n Informational: no key is looked up from it, and it establishes no trust.", "type": "string" }, "principal_id": { "default": "", - "description": "Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").", + "description": "Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").\n Informational; not read by the verifier.", "type": "string" }, "quota_period": { @@ -57,11 +57,11 @@ "type": "string" }, "revocation_uri": { - "description": "Optional: URI for real-time revocation checking.\n Exchange MAY check this for high-value transactions.\n Not checked for routine low-value access (performance tradeoff).", + "description": "Informational. Revocation lookup is not part of the delegation profile:\n the verifier never fetches this URI. A chain is invalidated early by\n withdrawing the configured root key.", "type": "string" }, "scopes": { - "description": "Scopes granted by this delegation. MUST be a subset of the\n principal's own scopes (attenuation — can only narrow, not widen).", + "description": "Mirror of the terminal token's effective scopes. In the chain, each\n child's scopes are covered by its parent's under the scope-matching rule\n on Requester.scopes (attenuation — can only narrow, not widen).", "items": { "type": "string" }, @@ -69,8 +69,9 @@ }, "token": { "default": null, - "description": "Token bytes. A JWT (base64url-encoded JWS).", - "pattern": "^[A-Za-z0-9+/]*={0,2}$", + "description": "Token bytes. A JWT (base64url-encoded JWS). At most 32768 bytes: the\n complete chain, every embedded parent included, must fit. The limit is a\n wire rule, checked before the token is parsed and independent of whether\n the bytes are a valid JWT; a longer token is an invalid request, not a\n per-item denial.", + "maxLength": 43692, + "pattern": "^[A-Za-z0-9+/]{0,43691}={0,2}$", "type": "string" }, "token_format": { @@ -120,7 +121,7 @@ "type": "string" }, "scopes": { - "description": "EXISTENCE HIDING. A requester never learns about a resource outside its\n scopes. When the scopes leave no presentable offer for a resource, the\n Exchange answers with no offers and no absence reason (an OfferGroup with\n empty `offers` and `absence_reason` unset), exactly as for any resource\n with nothing to offer. A Broker relaying that answer adds no reason of its\n own. At purchase, an offer the Exchange presented is honoured until it\n expires, so a scope refusal cannot arise there. No reason in the protocol\n reports a scope shortfall: OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT and\n DENIAL_REASON_SCOPE_INSUFFICIENT are deprecated and never sent.\n\n Scope format: colon-separated segments, \"{domain}:{permission}\" or\n \"{profile}:{permission}\", optionally multi-segment (\"dist:US:CA\");\n matching is segment-wise per the rule below (no implicit hierarchy).\n Examples:\n \"credit:read\" — can access credit reports\n \"subscription:marketdata-2026\" — has active MarketData subscription\n \"academic:*\" — full access to academic resources\n \"internal:reports\" — can access internal reports\n \"*\" — unrestricted (public Exchange default)\n\n Matching is SEGMENT-WISE (\":\" separated). A granted scope G covers a\n required scope R iff, segment by segment, each G segment equals the\n corresponding R segment or is \"*\"; a terminal \"*\" matches all remaining\n segments. There is NO implicit prefix match, and a grant NARROWER than\n the requirement does not cover it (G must be equal-to-or-broader than R).\n Examples: \"dist:*\" covers \"dist:US\" and \"dist:US:CA\"; \"dist:US:*\" covers\n \"dist:US:CA\" but not \"dist:EU\"; bare \"dist\" covers only \"dist\"; granted\n \"dist:US:CA\" does NOT cover required \"dist:US\"; \"*\" covers everything.\n This same rule governs LicenseTerm.scopes — one algorithm protocol-wide.\n\n When empty, Exchange applies its default access policy (typically\n returns all publicly available resources).", + "description": "EXISTENCE HIDING. A requester never learns about a resource outside its\n scopes. When the scopes leave no presentable offer for a resource, the\n Exchange answers with no offers and no absence reason (an OfferGroup with\n empty `offers` and `absence_reason` unset), exactly as for any resource\n with nothing to offer. A Broker relaying that answer adds no reason of its\n own. At purchase, an offer the Exchange presented is honoured until it\n expires, so a scope refusal cannot arise there. No reason in the protocol\n reports a scope shortfall: OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT and\n DENIAL_REASON_SCOPE_INSUFFICIENT are deprecated and never sent.\n\n Scope format: colon-separated segments, \"{domain}:{permission}\" or\n \"{profile}:{permission}\", optionally multi-segment (\"dist:US:CA\");\n matching is segment-wise per the rule below (no implicit hierarchy).\n Examples:\n \"credit:read\" — can access credit reports\n \"subscription:marketdata-2026\" — has active MarketData subscription\n \"academic:*\" — full access to academic resources\n \"internal:reports\" — can access internal reports\n \"*\" — unrestricted (public Exchange default)\n\n Matching is SEGMENT-WISE (\":\" separated). A granted scope G covers a\n required scope R iff, segment by segment, each G segment equals the\n corresponding R segment or is \"*\"; a terminal \"*\" matches all remaining\n segments, of which there must be at least one. There is NO implicit prefix\n match, and a grant NARROWER than the requirement does not cover it (G must\n be equal-to-or-broader than R), so a grant with more segments than the\n requirement never covers it, even when its last segment is \"*\". A \"*\" in R\n is a literal segment: a narrow grant never covers a wildcard requirement.\n Matching is case-sensitive and never trims. Examples: \"dist:*\" covers\n \"dist:US\", \"dist:US:CA\" and \"dist:US:*\" but not \"dist\"; \"dist:US:*\" covers\n \"dist:US:CA\" but not \"dist:EU\" or \"dist:US\"; \"publisher:*:read\" covers\n \"publisher:news:read\" but not \"publisher:news:*\"; bare \"dist\" covers only\n \"dist\"; granted \"dist:US:CA\" does NOT cover required \"dist:US\"; \"*\" covers\n everything. This same rule governs LicenseTerm.scopes and the attenuation\n of a delegation child against its parent — one algorithm protocol-wide.\n\n When empty, Exchange applies its default access policy (typically\n returns all publicly available resources).", "items": { "type": "string" }, diff --git a/gen/jsonschema/fora.v1.Requester.schema.strict.json b/gen/jsonschema/fora.v1.Requester.schema.strict.json index 295784da..d8659ca5 100644 --- a/gen/jsonschema/fora.v1.Requester.schema.strict.json +++ b/gen/jsonschema/fora.v1.Requester.schema.strict.json @@ -2,10 +2,10 @@ "$defs": { "fora.v1.Delegation": { "additionalProperties": false, - "description": "The token is an opaque, signed credential. The format is a JWT (token_format\n \"jwt\"); the field stays open for a future format. Its claims are the\n AUTHORITATIVE source of the grant; the\n plaintext fields below (scopes, expires_at, max_spend_cents, …) are a\n convenience mirror the Exchange MAY use for fast pre-filtering before it\n verifies the token.\n\n Holder binding (the load-bearing property). The grant is bound to a holder key\n via the RFC 7800 `cnf` confirmation claim — `cnf.jkt`, the RFC 7638 JWK\n thumbprint of the holder's key. Verification MUST check that the key signing\n the request (RFC 9421) hashes to that thumbprint; a token possessed without\n the matching private key is rejected. This is what makes a leaked token NOT\n bearer-usable. One exception, the relayed purchase: when a Broker re-packages\n a purchase (BrokerService.ExecuteTransaction), the request an Exchange\n receives is signed by the Broker, not by the holder. There the Exchange checks\n the holder binding against the key each item's AgentAcceptance verifies under\n — the agent's key, resolved from the requester's WBA directory — and not\n against the request signer. A relayed purchase that carries a delegation\n therefore needs an AgentAcceptance on every item; an item without one fails\n the binding (DENIAL_REASON_DELEGATION_INVALID). On a query a Broker\n originated (Broker-led discovery), the Broker signs alone, so a delegation on\n it cannot pass holder binding: the Exchange does not honour it and answers\n as if none were present. How an agent proves a holder-of-key entitlement at\n discovery time through a Broker is an open protocol question. An Exchange\n that requires a verified delegation before it presents a scoped term (see\n LicenseTerm.scopes) therefore presents no such term on that query.\n\n Delegation is a chain of cnf-linked JWTs: a principal narrows a\n grant by issuing a child JWT (cnf = the next holder, scopes ⊆ parent), signed\n by the key the parent's cnf named — the chain-linkage invariant. Verifiers\n need only the root issuer's public key; intermediate keys ride inside the\n chain (JOSE header `jwk`), so verification is offline.\n\n The claim schema is a small registered vocabulary — see the FORA\n delegation-claims profile in the auth spec. All claims are OPTIONAL except the\n holder binding (cnf). Vendors MAY add namespaced claims (their own \"vendor:\"\n namespace; \"fora_\"-prefixed names are reserved for the registered vocabulary\n and MUST NOT be redefined); any constraint a verifier cannot evaluate is\n binding by default (fail closed) unless the issuer marks it advisory —\n mirroring Restriction.advisory.\n\n Scope/time/spend caps are defense-in-depth that bound the blast radius only in\n the residual case where the holder's signing key is also compromised; the\n primary protection against theft is the holder binding above.", + "description": "The token is a signed JWT (token_format \"jwt\"; the field stays open for a\n future format) carrying a holder-bound delegation chain. Its claims are the\n AUTHORITATIVE source of the grant; the plaintext fields below (scopes,\n expires_at, max_spend_cents, …) are a mirror the Exchange MAY use for fast\n pre-filtering before it verifies the token. A mirror establishes nothing: it\n cannot grant a scope, extend validity or raise a cap beyond what the signed\n chain says.\n\n The profile — chain encoding and limits, protected header, claims, clock\n leeway, issuer, audience and holder-binding rules, the verification\n procedure and the outcome of each failure — is the \"Delegation\" section of\n the authentication page and is not restated here; the Go SDK verifier\n (sdk/go/helpers) and the shared chain cases are derived from it. What a\n field reader must know: the grant is bound to a holder key by `cnf.jkt`, and\n the key a request proves possession of MUST hash to the terminal token's\n `cnf.jkt` (the RFC 9421 request signer on a direct call; on a purchase a\n Broker relayed, the key each item's AgentAcceptance verifies under), so a\n token possessed without its private key grants nothing. On a query a Broker\n originated nothing can pass that binding: the Exchange does not honour the\n delegation, answers as if none were present, and presents no term gated\n behind one (see LicenseTerm.scopes).", "properties": { "expires_at": { - "description": "When this delegation expires. Exchange MUST reject expired tokens.", + "description": "Mirror of the chain's earliest `exp`. The Exchange MUST reject an expired\n token; the signed `exp` values decide, not this field.", "format": "date-time", "type": "string" }, @@ -21,7 +21,7 @@ "type": "array" }, "issuer": { - "description": "Token issuer. OIDC issuer URL or GNAP grant server URL.\n Exchange uses this for JWT validation (OIDC discovery → JWKS)\n or GNAP token introspection.", + "description": "Mirror of the ROOT token's `iss`, the identity configured with the trusted\n key (never an intermediate or terminal issuer). Informational: the\n Exchange never discovers a key from it, through OIDC, JWKS or otherwise.", "type": "string" }, "max_accesses": { @@ -44,12 +44,12 @@ }, "principal_domain": { "default": "", - "description": "Who granted this delegation (domain for public key lookup).", + "description": "Mirror of the ROOT token's `iss`, the configured trust anchor's identity.\n Informational: no key is looked up from it, and it establishes no trust.", "type": "string" }, "principal_id": { "default": "", - "description": "Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").", + "description": "Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").\n Informational; not read by the verifier.", "type": "string" }, "quota_period": { @@ -58,11 +58,11 @@ "type": "string" }, "revocation_uri": { - "description": "Optional: URI for real-time revocation checking.\n Exchange MAY check this for high-value transactions.\n Not checked for routine low-value access (performance tradeoff).", + "description": "Informational. Revocation lookup is not part of the delegation profile:\n the verifier never fetches this URI. A chain is invalidated early by\n withdrawing the configured root key.", "type": "string" }, "scopes": { - "description": "Scopes granted by this delegation. MUST be a subset of the\n principal's own scopes (attenuation — can only narrow, not widen).", + "description": "Mirror of the terminal token's effective scopes. In the chain, each\n child's scopes are covered by its parent's under the scope-matching rule\n on Requester.scopes (attenuation — can only narrow, not widen).", "items": { "type": "string" }, @@ -70,8 +70,9 @@ }, "token": { "default": null, - "description": "Token bytes. A JWT (base64url-encoded JWS).", - "pattern": "^[A-Za-z0-9+/]*={0,2}$", + "description": "Token bytes. A JWT (base64url-encoded JWS). At most 32768 bytes: the\n complete chain, every embedded parent included, must fit. The limit is a\n wire rule, checked before the token is parsed and independent of whether\n the bytes are a valid JWT; a longer token is an invalid request, not a\n per-item denial.", + "maxLength": 43692, + "pattern": "^[A-Za-z0-9+/]{0,43691}={0,2}$", "type": "string" }, "token_format": { @@ -122,7 +123,7 @@ "type": "string" }, "scopes": { - "description": "EXISTENCE HIDING. A requester never learns about a resource outside its\n scopes. When the scopes leave no presentable offer for a resource, the\n Exchange answers with no offers and no absence reason (an OfferGroup with\n empty `offers` and `absence_reason` unset), exactly as for any resource\n with nothing to offer. A Broker relaying that answer adds no reason of its\n own. At purchase, an offer the Exchange presented is honoured until it\n expires, so a scope refusal cannot arise there. No reason in the protocol\n reports a scope shortfall: OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT and\n DENIAL_REASON_SCOPE_INSUFFICIENT are deprecated and never sent.\n\n Scope format: colon-separated segments, \"{domain}:{permission}\" or\n \"{profile}:{permission}\", optionally multi-segment (\"dist:US:CA\");\n matching is segment-wise per the rule below (no implicit hierarchy).\n Examples:\n \"credit:read\" — can access credit reports\n \"subscription:marketdata-2026\" — has active MarketData subscription\n \"academic:*\" — full access to academic resources\n \"internal:reports\" — can access internal reports\n \"*\" — unrestricted (public Exchange default)\n\n Matching is SEGMENT-WISE (\":\" separated). A granted scope G covers a\n required scope R iff, segment by segment, each G segment equals the\n corresponding R segment or is \"*\"; a terminal \"*\" matches all remaining\n segments. There is NO implicit prefix match, and a grant NARROWER than\n the requirement does not cover it (G must be equal-to-or-broader than R).\n Examples: \"dist:*\" covers \"dist:US\" and \"dist:US:CA\"; \"dist:US:*\" covers\n \"dist:US:CA\" but not \"dist:EU\"; bare \"dist\" covers only \"dist\"; granted\n \"dist:US:CA\" does NOT cover required \"dist:US\"; \"*\" covers everything.\n This same rule governs LicenseTerm.scopes — one algorithm protocol-wide.\n\n When empty, Exchange applies its default access policy (typically\n returns all publicly available resources).", + "description": "EXISTENCE HIDING. A requester never learns about a resource outside its\n scopes. When the scopes leave no presentable offer for a resource, the\n Exchange answers with no offers and no absence reason (an OfferGroup with\n empty `offers` and `absence_reason` unset), exactly as for any resource\n with nothing to offer. A Broker relaying that answer adds no reason of its\n own. At purchase, an offer the Exchange presented is honoured until it\n expires, so a scope refusal cannot arise there. No reason in the protocol\n reports a scope shortfall: OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT and\n DENIAL_REASON_SCOPE_INSUFFICIENT are deprecated and never sent.\n\n Scope format: colon-separated segments, \"{domain}:{permission}\" or\n \"{profile}:{permission}\", optionally multi-segment (\"dist:US:CA\");\n matching is segment-wise per the rule below (no implicit hierarchy).\n Examples:\n \"credit:read\" — can access credit reports\n \"subscription:marketdata-2026\" — has active MarketData subscription\n \"academic:*\" — full access to academic resources\n \"internal:reports\" — can access internal reports\n \"*\" — unrestricted (public Exchange default)\n\n Matching is SEGMENT-WISE (\":\" separated). A granted scope G covers a\n required scope R iff, segment by segment, each G segment equals the\n corresponding R segment or is \"*\"; a terminal \"*\" matches all remaining\n segments, of which there must be at least one. There is NO implicit prefix\n match, and a grant NARROWER than the requirement does not cover it (G must\n be equal-to-or-broader than R), so a grant with more segments than the\n requirement never covers it, even when its last segment is \"*\". A \"*\" in R\n is a literal segment: a narrow grant never covers a wildcard requirement.\n Matching is case-sensitive and never trims. Examples: \"dist:*\" covers\n \"dist:US\", \"dist:US:CA\" and \"dist:US:*\" but not \"dist\"; \"dist:US:*\" covers\n \"dist:US:CA\" but not \"dist:EU\" or \"dist:US\"; \"publisher:*:read\" covers\n \"publisher:news:read\" but not \"publisher:news:*\"; bare \"dist\" covers only\n \"dist\"; granted \"dist:US:CA\" does NOT cover required \"dist:US\"; \"*\" covers\n everything. This same rule governs LicenseTerm.scopes and the attenuation\n of a delegation child against its parent — one algorithm protocol-wide.\n\n When empty, Exchange applies its default access policy (typically\n returns all publicly available resources).", "items": { "type": "string" }, diff --git a/gen/jsonschema/fora.v1.ResourceEntry.schema.json b/gen/jsonschema/fora.v1.ResourceEntry.schema.json index f22ec0ad..e90edd6f 100644 --- a/gen/jsonschema/fora.v1.ResourceEntry.schema.json +++ b/gen/jsonschema/fora.v1.ResourceEntry.schema.json @@ -71,7 +71,7 @@ "type": "array" }, "scopes": { - "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n also require a verified delegation that grants these scopes before it\n presents the term. To a requester that presents none, it answers as if the\n term did not exist, as existence hiding requires (see Requester.scopes). An\n offer the Exchange presented is honoured until it expires.\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", + "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n instead gate a term behind a verified delegation (a subscription term is\n the usual case): then only the delegation chain's verified effective scopes\n are matched against these scopes, and Requester.scopes is ignored for that\n term. To a requester that presents no valid delegation, it answers as if\n the term did not exist, as existence hiding requires (see\n Requester.scopes). An offer the Exchange presented is honoured until it\n expires, and a gated offer expires no later than the chain's effective\n expiry (see Delegation).\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments (at least one), and there is NO implicit prefix match\n (a grant narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", "items": { "type": "string" }, diff --git a/gen/jsonschema/fora.v1.ResourceEntry.schema.strict.json b/gen/jsonschema/fora.v1.ResourceEntry.schema.strict.json index f23d7e76..661936db 100644 --- a/gen/jsonschema/fora.v1.ResourceEntry.schema.strict.json +++ b/gen/jsonschema/fora.v1.ResourceEntry.schema.strict.json @@ -73,7 +73,7 @@ "type": "array" }, "scopes": { - "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n also require a verified delegation that grants these scopes before it\n presents the term. To a requester that presents none, it answers as if the\n term did not exist, as existence hiding requires (see Requester.scopes). An\n offer the Exchange presented is honoured until it expires.\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", + "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n instead gate a term behind a verified delegation (a subscription term is\n the usual case): then only the delegation chain's verified effective scopes\n are matched against these scopes, and Requester.scopes is ignored for that\n term. To a requester that presents no valid delegation, it answers as if\n the term did not exist, as existence hiding requires (see\n Requester.scopes). An offer the Exchange presented is honoured until it\n expires, and a gated offer expires no later than the chain's effective\n expiry (see Delegation).\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments (at least one), and there is NO implicit prefix match\n (a grant narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", "items": { "type": "string" }, diff --git a/gen/jsonschema/fora.v1.ResourceQuery.schema.json b/gen/jsonschema/fora.v1.ResourceQuery.schema.json index e6a3a8f2..1acd7e28 100644 --- a/gen/jsonschema/fora.v1.ResourceQuery.schema.json +++ b/gen/jsonschema/fora.v1.ResourceQuery.schema.json @@ -44,10 +44,10 @@ "type": "object" }, "fora.v1.Delegation": { - "description": "The token is an opaque, signed credential. The format is a JWT (token_format\n \"jwt\"); the field stays open for a future format. Its claims are the\n AUTHORITATIVE source of the grant; the\n plaintext fields below (scopes, expires_at, max_spend_cents, …) are a\n convenience mirror the Exchange MAY use for fast pre-filtering before it\n verifies the token.\n\n Holder binding (the load-bearing property). The grant is bound to a holder key\n via the RFC 7800 `cnf` confirmation claim — `cnf.jkt`, the RFC 7638 JWK\n thumbprint of the holder's key. Verification MUST check that the key signing\n the request (RFC 9421) hashes to that thumbprint; a token possessed without\n the matching private key is rejected. This is what makes a leaked token NOT\n bearer-usable. One exception, the relayed purchase: when a Broker re-packages\n a purchase (BrokerService.ExecuteTransaction), the request an Exchange\n receives is signed by the Broker, not by the holder. There the Exchange checks\n the holder binding against the key each item's AgentAcceptance verifies under\n — the agent's key, resolved from the requester's WBA directory — and not\n against the request signer. A relayed purchase that carries a delegation\n therefore needs an AgentAcceptance on every item; an item without one fails\n the binding (DENIAL_REASON_DELEGATION_INVALID). On a query a Broker\n originated (Broker-led discovery), the Broker signs alone, so a delegation on\n it cannot pass holder binding: the Exchange does not honour it and answers\n as if none were present. How an agent proves a holder-of-key entitlement at\n discovery time through a Broker is an open protocol question. An Exchange\n that requires a verified delegation before it presents a scoped term (see\n LicenseTerm.scopes) therefore presents no such term on that query.\n\n Delegation is a chain of cnf-linked JWTs: a principal narrows a\n grant by issuing a child JWT (cnf = the next holder, scopes ⊆ parent), signed\n by the key the parent's cnf named — the chain-linkage invariant. Verifiers\n need only the root issuer's public key; intermediate keys ride inside the\n chain (JOSE header `jwk`), so verification is offline.\n\n The claim schema is a small registered vocabulary — see the FORA\n delegation-claims profile in the auth spec. All claims are OPTIONAL except the\n holder binding (cnf). Vendors MAY add namespaced claims (their own \"vendor:\"\n namespace; \"fora_\"-prefixed names are reserved for the registered vocabulary\n and MUST NOT be redefined); any constraint a verifier cannot evaluate is\n binding by default (fail closed) unless the issuer marks it advisory —\n mirroring Restriction.advisory.\n\n Scope/time/spend caps are defense-in-depth that bound the blast radius only in\n the residual case where the holder's signing key is also compromised; the\n primary protection against theft is the holder binding above.", + "description": "The token is a signed JWT (token_format \"jwt\"; the field stays open for a\n future format) carrying a holder-bound delegation chain. Its claims are the\n AUTHORITATIVE source of the grant; the plaintext fields below (scopes,\n expires_at, max_spend_cents, …) are a mirror the Exchange MAY use for fast\n pre-filtering before it verifies the token. A mirror establishes nothing: it\n cannot grant a scope, extend validity or raise a cap beyond what the signed\n chain says.\n\n The profile — chain encoding and limits, protected header, claims, clock\n leeway, issuer, audience and holder-binding rules, the verification\n procedure and the outcome of each failure — is the \"Delegation\" section of\n the authentication page and is not restated here; the Go SDK verifier\n (sdk/go/helpers) and the shared chain cases are derived from it. What a\n field reader must know: the grant is bound to a holder key by `cnf.jkt`, and\n the key a request proves possession of MUST hash to the terminal token's\n `cnf.jkt` (the RFC 9421 request signer on a direct call; on a purchase a\n Broker relayed, the key each item's AgentAcceptance verifies under), so a\n token possessed without its private key grants nothing. On a query a Broker\n originated nothing can pass that binding: the Exchange does not honour the\n delegation, answers as if none were present, and presents no term gated\n behind one (see LicenseTerm.scopes).", "properties": { "expires_at": { - "description": "When this delegation expires. Exchange MUST reject expired tokens.", + "description": "Mirror of the chain's earliest `exp`. The Exchange MUST reject an expired\n token; the signed `exp` values decide, not this field.", "format": "date-time", "type": "string" }, @@ -63,7 +63,7 @@ "type": "array" }, "issuer": { - "description": "Token issuer. OIDC issuer URL or GNAP grant server URL.\n Exchange uses this for JWT validation (OIDC discovery → JWKS)\n or GNAP token introspection.", + "description": "Mirror of the ROOT token's `iss`, the identity configured with the trusted\n key (never an intermediate or terminal issuer). Informational: the\n Exchange never discovers a key from it, through OIDC, JWKS or otherwise.", "type": "string" }, "max_accesses": { @@ -86,12 +86,12 @@ }, "principal_domain": { "default": "", - "description": "Who granted this delegation (domain for public key lookup).", + "description": "Mirror of the ROOT token's `iss`, the configured trust anchor's identity.\n Informational: no key is looked up from it, and it establishes no trust.", "type": "string" }, "principal_id": { "default": "", - "description": "Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").", + "description": "Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").\n Informational; not read by the verifier.", "type": "string" }, "quota_period": { @@ -100,11 +100,11 @@ "type": "string" }, "revocation_uri": { - "description": "Optional: URI for real-time revocation checking.\n Exchange MAY check this for high-value transactions.\n Not checked for routine low-value access (performance tradeoff).", + "description": "Informational. Revocation lookup is not part of the delegation profile:\n the verifier never fetches this URI. A chain is invalidated early by\n withdrawing the configured root key.", "type": "string" }, "scopes": { - "description": "Scopes granted by this delegation. MUST be a subset of the\n principal's own scopes (attenuation — can only narrow, not widen).", + "description": "Mirror of the terminal token's effective scopes. In the chain, each\n child's scopes are covered by its parent's under the scope-matching rule\n on Requester.scopes (attenuation — can only narrow, not widen).", "items": { "type": "string" }, @@ -112,8 +112,9 @@ }, "token": { "default": null, - "description": "Token bytes. A JWT (base64url-encoded JWS).", - "pattern": "^[A-Za-z0-9+/]*={0,2}$", + "description": "Token bytes. A JWT (base64url-encoded JWS). At most 32768 bytes: the\n complete chain, every embedded parent included, must fit. The limit is a\n wire rule, checked before the token is parsed and independent of whether\n the bytes are a valid JWT; a longer token is an invalid request, not a\n per-item denial.", + "maxLength": 43692, + "pattern": "^[A-Za-z0-9+/]{0,43691}={0,2}$", "type": "string" }, "token_format": { @@ -161,7 +162,7 @@ "type": "string" }, "scopes": { - "description": "EXISTENCE HIDING. A requester never learns about a resource outside its\n scopes. When the scopes leave no presentable offer for a resource, the\n Exchange answers with no offers and no absence reason (an OfferGroup with\n empty `offers` and `absence_reason` unset), exactly as for any resource\n with nothing to offer. A Broker relaying that answer adds no reason of its\n own. At purchase, an offer the Exchange presented is honoured until it\n expires, so a scope refusal cannot arise there. No reason in the protocol\n reports a scope shortfall: OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT and\n DENIAL_REASON_SCOPE_INSUFFICIENT are deprecated and never sent.\n\n Scope format: colon-separated segments, \"{domain}:{permission}\" or\n \"{profile}:{permission}\", optionally multi-segment (\"dist:US:CA\");\n matching is segment-wise per the rule below (no implicit hierarchy).\n Examples:\n \"credit:read\" — can access credit reports\n \"subscription:marketdata-2026\" — has active MarketData subscription\n \"academic:*\" — full access to academic resources\n \"internal:reports\" — can access internal reports\n \"*\" — unrestricted (public Exchange default)\n\n Matching is SEGMENT-WISE (\":\" separated). A granted scope G covers a\n required scope R iff, segment by segment, each G segment equals the\n corresponding R segment or is \"*\"; a terminal \"*\" matches all remaining\n segments. There is NO implicit prefix match, and a grant NARROWER than\n the requirement does not cover it (G must be equal-to-or-broader than R).\n Examples: \"dist:*\" covers \"dist:US\" and \"dist:US:CA\"; \"dist:US:*\" covers\n \"dist:US:CA\" but not \"dist:EU\"; bare \"dist\" covers only \"dist\"; granted\n \"dist:US:CA\" does NOT cover required \"dist:US\"; \"*\" covers everything.\n This same rule governs LicenseTerm.scopes — one algorithm protocol-wide.\n\n When empty, Exchange applies its default access policy (typically\n returns all publicly available resources).", + "description": "EXISTENCE HIDING. A requester never learns about a resource outside its\n scopes. When the scopes leave no presentable offer for a resource, the\n Exchange answers with no offers and no absence reason (an OfferGroup with\n empty `offers` and `absence_reason` unset), exactly as for any resource\n with nothing to offer. A Broker relaying that answer adds no reason of its\n own. At purchase, an offer the Exchange presented is honoured until it\n expires, so a scope refusal cannot arise there. No reason in the protocol\n reports a scope shortfall: OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT and\n DENIAL_REASON_SCOPE_INSUFFICIENT are deprecated and never sent.\n\n Scope format: colon-separated segments, \"{domain}:{permission}\" or\n \"{profile}:{permission}\", optionally multi-segment (\"dist:US:CA\");\n matching is segment-wise per the rule below (no implicit hierarchy).\n Examples:\n \"credit:read\" — can access credit reports\n \"subscription:marketdata-2026\" — has active MarketData subscription\n \"academic:*\" — full access to academic resources\n \"internal:reports\" — can access internal reports\n \"*\" — unrestricted (public Exchange default)\n\n Matching is SEGMENT-WISE (\":\" separated). A granted scope G covers a\n required scope R iff, segment by segment, each G segment equals the\n corresponding R segment or is \"*\"; a terminal \"*\" matches all remaining\n segments, of which there must be at least one. There is NO implicit prefix\n match, and a grant NARROWER than the requirement does not cover it (G must\n be equal-to-or-broader than R), so a grant with more segments than the\n requirement never covers it, even when its last segment is \"*\". A \"*\" in R\n is a literal segment: a narrow grant never covers a wildcard requirement.\n Matching is case-sensitive and never trims. Examples: \"dist:*\" covers\n \"dist:US\", \"dist:US:CA\" and \"dist:US:*\" but not \"dist\"; \"dist:US:*\" covers\n \"dist:US:CA\" but not \"dist:EU\" or \"dist:US\"; \"publisher:*:read\" covers\n \"publisher:news:read\" but not \"publisher:news:*\"; bare \"dist\" covers only\n \"dist\"; granted \"dist:US:CA\" does NOT cover required \"dist:US\"; \"*\" covers\n everything. This same rule governs LicenseTerm.scopes and the attenuation\n of a delegation child against its parent — one algorithm protocol-wide.\n\n When empty, Exchange applies its default access policy (typically\n returns all publicly available resources).", "items": { "type": "string" }, diff --git a/gen/jsonschema/fora.v1.ResourceQuery.schema.strict.json b/gen/jsonschema/fora.v1.ResourceQuery.schema.strict.json index 0ac5acd4..8ad66a04 100644 --- a/gen/jsonschema/fora.v1.ResourceQuery.schema.strict.json +++ b/gen/jsonschema/fora.v1.ResourceQuery.schema.strict.json @@ -46,10 +46,10 @@ }, "fora.v1.Delegation": { "additionalProperties": false, - "description": "The token is an opaque, signed credential. The format is a JWT (token_format\n \"jwt\"); the field stays open for a future format. Its claims are the\n AUTHORITATIVE source of the grant; the\n plaintext fields below (scopes, expires_at, max_spend_cents, …) are a\n convenience mirror the Exchange MAY use for fast pre-filtering before it\n verifies the token.\n\n Holder binding (the load-bearing property). The grant is bound to a holder key\n via the RFC 7800 `cnf` confirmation claim — `cnf.jkt`, the RFC 7638 JWK\n thumbprint of the holder's key. Verification MUST check that the key signing\n the request (RFC 9421) hashes to that thumbprint; a token possessed without\n the matching private key is rejected. This is what makes a leaked token NOT\n bearer-usable. One exception, the relayed purchase: when a Broker re-packages\n a purchase (BrokerService.ExecuteTransaction), the request an Exchange\n receives is signed by the Broker, not by the holder. There the Exchange checks\n the holder binding against the key each item's AgentAcceptance verifies under\n — the agent's key, resolved from the requester's WBA directory — and not\n against the request signer. A relayed purchase that carries a delegation\n therefore needs an AgentAcceptance on every item; an item without one fails\n the binding (DENIAL_REASON_DELEGATION_INVALID). On a query a Broker\n originated (Broker-led discovery), the Broker signs alone, so a delegation on\n it cannot pass holder binding: the Exchange does not honour it and answers\n as if none were present. How an agent proves a holder-of-key entitlement at\n discovery time through a Broker is an open protocol question. An Exchange\n that requires a verified delegation before it presents a scoped term (see\n LicenseTerm.scopes) therefore presents no such term on that query.\n\n Delegation is a chain of cnf-linked JWTs: a principal narrows a\n grant by issuing a child JWT (cnf = the next holder, scopes ⊆ parent), signed\n by the key the parent's cnf named — the chain-linkage invariant. Verifiers\n need only the root issuer's public key; intermediate keys ride inside the\n chain (JOSE header `jwk`), so verification is offline.\n\n The claim schema is a small registered vocabulary — see the FORA\n delegation-claims profile in the auth spec. All claims are OPTIONAL except the\n holder binding (cnf). Vendors MAY add namespaced claims (their own \"vendor:\"\n namespace; \"fora_\"-prefixed names are reserved for the registered vocabulary\n and MUST NOT be redefined); any constraint a verifier cannot evaluate is\n binding by default (fail closed) unless the issuer marks it advisory —\n mirroring Restriction.advisory.\n\n Scope/time/spend caps are defense-in-depth that bound the blast radius only in\n the residual case where the holder's signing key is also compromised; the\n primary protection against theft is the holder binding above.", + "description": "The token is a signed JWT (token_format \"jwt\"; the field stays open for a\n future format) carrying a holder-bound delegation chain. Its claims are the\n AUTHORITATIVE source of the grant; the plaintext fields below (scopes,\n expires_at, max_spend_cents, …) are a mirror the Exchange MAY use for fast\n pre-filtering before it verifies the token. A mirror establishes nothing: it\n cannot grant a scope, extend validity or raise a cap beyond what the signed\n chain says.\n\n The profile — chain encoding and limits, protected header, claims, clock\n leeway, issuer, audience and holder-binding rules, the verification\n procedure and the outcome of each failure — is the \"Delegation\" section of\n the authentication page and is not restated here; the Go SDK verifier\n (sdk/go/helpers) and the shared chain cases are derived from it. What a\n field reader must know: the grant is bound to a holder key by `cnf.jkt`, and\n the key a request proves possession of MUST hash to the terminal token's\n `cnf.jkt` (the RFC 9421 request signer on a direct call; on a purchase a\n Broker relayed, the key each item's AgentAcceptance verifies under), so a\n token possessed without its private key grants nothing. On a query a Broker\n originated nothing can pass that binding: the Exchange does not honour the\n delegation, answers as if none were present, and presents no term gated\n behind one (see LicenseTerm.scopes).", "properties": { "expires_at": { - "description": "When this delegation expires. Exchange MUST reject expired tokens.", + "description": "Mirror of the chain's earliest `exp`. The Exchange MUST reject an expired\n token; the signed `exp` values decide, not this field.", "format": "date-time", "type": "string" }, @@ -65,7 +65,7 @@ "type": "array" }, "issuer": { - "description": "Token issuer. OIDC issuer URL or GNAP grant server URL.\n Exchange uses this for JWT validation (OIDC discovery → JWKS)\n or GNAP token introspection.", + "description": "Mirror of the ROOT token's `iss`, the identity configured with the trusted\n key (never an intermediate or terminal issuer). Informational: the\n Exchange never discovers a key from it, through OIDC, JWKS or otherwise.", "type": "string" }, "max_accesses": { @@ -88,12 +88,12 @@ }, "principal_domain": { "default": "", - "description": "Who granted this delegation (domain for public key lookup).", + "description": "Mirror of the ROOT token's `iss`, the configured trust anchor's identity.\n Informational: no key is looked up from it, and it establishes no trust.", "type": "string" }, "principal_id": { "default": "", - "description": "Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").", + "description": "Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").\n Informational; not read by the verifier.", "type": "string" }, "quota_period": { @@ -102,11 +102,11 @@ "type": "string" }, "revocation_uri": { - "description": "Optional: URI for real-time revocation checking.\n Exchange MAY check this for high-value transactions.\n Not checked for routine low-value access (performance tradeoff).", + "description": "Informational. Revocation lookup is not part of the delegation profile:\n the verifier never fetches this URI. A chain is invalidated early by\n withdrawing the configured root key.", "type": "string" }, "scopes": { - "description": "Scopes granted by this delegation. MUST be a subset of the\n principal's own scopes (attenuation — can only narrow, not widen).", + "description": "Mirror of the terminal token's effective scopes. In the chain, each\n child's scopes are covered by its parent's under the scope-matching rule\n on Requester.scopes (attenuation — can only narrow, not widen).", "items": { "type": "string" }, @@ -114,8 +114,9 @@ }, "token": { "default": null, - "description": "Token bytes. A JWT (base64url-encoded JWS).", - "pattern": "^[A-Za-z0-9+/]*={0,2}$", + "description": "Token bytes. A JWT (base64url-encoded JWS). At most 32768 bytes: the\n complete chain, every embedded parent included, must fit. The limit is a\n wire rule, checked before the token is parsed and independent of whether\n the bytes are a valid JWT; a longer token is an invalid request, not a\n per-item denial.", + "maxLength": 43692, + "pattern": "^[A-Za-z0-9+/]{0,43691}={0,2}$", "type": "string" }, "token_format": { @@ -164,7 +165,7 @@ "type": "string" }, "scopes": { - "description": "EXISTENCE HIDING. A requester never learns about a resource outside its\n scopes. When the scopes leave no presentable offer for a resource, the\n Exchange answers with no offers and no absence reason (an OfferGroup with\n empty `offers` and `absence_reason` unset), exactly as for any resource\n with nothing to offer. A Broker relaying that answer adds no reason of its\n own. At purchase, an offer the Exchange presented is honoured until it\n expires, so a scope refusal cannot arise there. No reason in the protocol\n reports a scope shortfall: OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT and\n DENIAL_REASON_SCOPE_INSUFFICIENT are deprecated and never sent.\n\n Scope format: colon-separated segments, \"{domain}:{permission}\" or\n \"{profile}:{permission}\", optionally multi-segment (\"dist:US:CA\");\n matching is segment-wise per the rule below (no implicit hierarchy).\n Examples:\n \"credit:read\" — can access credit reports\n \"subscription:marketdata-2026\" — has active MarketData subscription\n \"academic:*\" — full access to academic resources\n \"internal:reports\" — can access internal reports\n \"*\" — unrestricted (public Exchange default)\n\n Matching is SEGMENT-WISE (\":\" separated). A granted scope G covers a\n required scope R iff, segment by segment, each G segment equals the\n corresponding R segment or is \"*\"; a terminal \"*\" matches all remaining\n segments. There is NO implicit prefix match, and a grant NARROWER than\n the requirement does not cover it (G must be equal-to-or-broader than R).\n Examples: \"dist:*\" covers \"dist:US\" and \"dist:US:CA\"; \"dist:US:*\" covers\n \"dist:US:CA\" but not \"dist:EU\"; bare \"dist\" covers only \"dist\"; granted\n \"dist:US:CA\" does NOT cover required \"dist:US\"; \"*\" covers everything.\n This same rule governs LicenseTerm.scopes — one algorithm protocol-wide.\n\n When empty, Exchange applies its default access policy (typically\n returns all publicly available resources).", + "description": "EXISTENCE HIDING. A requester never learns about a resource outside its\n scopes. When the scopes leave no presentable offer for a resource, the\n Exchange answers with no offers and no absence reason (an OfferGroup with\n empty `offers` and `absence_reason` unset), exactly as for any resource\n with nothing to offer. A Broker relaying that answer adds no reason of its\n own. At purchase, an offer the Exchange presented is honoured until it\n expires, so a scope refusal cannot arise there. No reason in the protocol\n reports a scope shortfall: OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT and\n DENIAL_REASON_SCOPE_INSUFFICIENT are deprecated and never sent.\n\n Scope format: colon-separated segments, \"{domain}:{permission}\" or\n \"{profile}:{permission}\", optionally multi-segment (\"dist:US:CA\");\n matching is segment-wise per the rule below (no implicit hierarchy).\n Examples:\n \"credit:read\" — can access credit reports\n \"subscription:marketdata-2026\" — has active MarketData subscription\n \"academic:*\" — full access to academic resources\n \"internal:reports\" — can access internal reports\n \"*\" — unrestricted (public Exchange default)\n\n Matching is SEGMENT-WISE (\":\" separated). A granted scope G covers a\n required scope R iff, segment by segment, each G segment equals the\n corresponding R segment or is \"*\"; a terminal \"*\" matches all remaining\n segments, of which there must be at least one. There is NO implicit prefix\n match, and a grant NARROWER than the requirement does not cover it (G must\n be equal-to-or-broader than R), so a grant with more segments than the\n requirement never covers it, even when its last segment is \"*\". A \"*\" in R\n is a literal segment: a narrow grant never covers a wildcard requirement.\n Matching is case-sensitive and never trims. Examples: \"dist:*\" covers\n \"dist:US\", \"dist:US:CA\" and \"dist:US:*\" but not \"dist\"; \"dist:US:*\" covers\n \"dist:US:CA\" but not \"dist:EU\" or \"dist:US\"; \"publisher:*:read\" covers\n \"publisher:news:read\" but not \"publisher:news:*\"; bare \"dist\" covers only\n \"dist\"; granted \"dist:US:CA\" does NOT cover required \"dist:US\"; \"*\" covers\n everything. This same rule governs LicenseTerm.scopes and the attenuation\n of a delegation child against its parent — one algorithm protocol-wide.\n\n When empty, Exchange applies its default access policy (typically\n returns all publicly available resources).", "items": { "type": "string" }, diff --git a/gen/jsonschema/fora.v1.ResourceResponse.schema.json b/gen/jsonschema/fora.v1.ResourceResponse.schema.json index d8c4dc56..86af23aa 100644 --- a/gen/jsonschema/fora.v1.ResourceResponse.schema.json +++ b/gen/jsonschema/fora.v1.ResourceResponse.schema.json @@ -71,7 +71,7 @@ "type": "array" }, "scopes": { - "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n also require a verified delegation that grants these scopes before it\n presents the term. To a requester that presents none, it answers as if the\n term did not exist, as existence hiding requires (see Requester.scopes). An\n offer the Exchange presented is honoured until it expires.\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", + "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n instead gate a term behind a verified delegation (a subscription term is\n the usual case): then only the delegation chain's verified effective scopes\n are matched against these scopes, and Requester.scopes is ignored for that\n term. To a requester that presents no valid delegation, it answers as if\n the term did not exist, as existence hiding requires (see\n Requester.scopes). An offer the Exchange presented is honoured until it\n expires, and a gated offer expires no later than the chain's effective\n expiry (see Delegation).\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments (at least one), and there is NO implicit prefix match\n (a grant narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", "items": { "type": "string" }, @@ -213,7 +213,7 @@ "type": "string" }, "expires_at": { - "description": "When this offer expires (ISO 8601).", + "description": "When this offer expires (ISO 8601). An offer for a term gated behind a\n verified delegation MUST NOT expire later than that delegation chain's\n effective expiry, the earliest `exp` in the chain (see Delegation).", "format": "date-time", "type": "string" }, diff --git a/gen/jsonschema/fora.v1.ResourceResponse.schema.strict.json b/gen/jsonschema/fora.v1.ResourceResponse.schema.strict.json index 963b09c9..6298b8be 100644 --- a/gen/jsonschema/fora.v1.ResourceResponse.schema.strict.json +++ b/gen/jsonschema/fora.v1.ResourceResponse.schema.strict.json @@ -73,7 +73,7 @@ "type": "array" }, "scopes": { - "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n also require a verified delegation that grants these scopes before it\n presents the term. To a requester that presents none, it answers as if the\n term did not exist, as existence hiding requires (see Requester.scopes). An\n offer the Exchange presented is honoured until it expires.\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", + "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n instead gate a term behind a verified delegation (a subscription term is\n the usual case): then only the delegation chain's verified effective scopes\n are matched against these scopes, and Requester.scopes is ignored for that\n term. To a requester that presents no valid delegation, it answers as if\n the term did not exist, as existence hiding requires (see\n Requester.scopes). An offer the Exchange presented is honoured until it\n expires, and a gated offer expires no later than the chain's effective\n expiry (see Delegation).\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments (at least one), and there is NO implicit prefix match\n (a grant narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", "items": { "type": "string" }, @@ -217,7 +217,7 @@ "type": "string" }, "expires_at": { - "description": "When this offer expires (ISO 8601).", + "description": "When this offer expires (ISO 8601). An offer for a term gated behind a\n verified delegation MUST NOT expire later than that delegation chain's\n effective expiry, the earliest `exp` in the chain (see Delegation).", "format": "date-time", "type": "string" }, diff --git a/gen/jsonschema/fora.v1.TransactionItem.schema.json b/gen/jsonschema/fora.v1.TransactionItem.schema.json index 5ba72e2b..e4f1072b 100644 --- a/gen/jsonschema/fora.v1.TransactionItem.schema.json +++ b/gen/jsonschema/fora.v1.TransactionItem.schema.json @@ -91,7 +91,7 @@ "type": "array" }, "scopes": { - "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n also require a verified delegation that grants these scopes before it\n presents the term. To a requester that presents none, it answers as if the\n term did not exist, as existence hiding requires (see Requester.scopes). An\n offer the Exchange presented is honoured until it expires.\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", + "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n instead gate a term behind a verified delegation (a subscription term is\n the usual case): then only the delegation chain's verified effective scopes\n are matched against these scopes, and Requester.scopes is ignored for that\n term. To a requester that presents no valid delegation, it answers as if\n the term did not exist, as existence hiding requires (see\n Requester.scopes). An offer the Exchange presented is honoured until it\n expires, and a gated offer expires no later than the chain's effective\n expiry (see Delegation).\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments (at least one), and there is NO implicit prefix match\n (a grant narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", "items": { "type": "string" }, @@ -233,7 +233,7 @@ "type": "string" }, "expires_at": { - "description": "When this offer expires (ISO 8601).", + "description": "When this offer expires (ISO 8601). An offer for a term gated behind a\n verified delegation MUST NOT expire later than that delegation chain's\n effective expiry, the earliest `exp` in the chain (see Delegation).", "format": "date-time", "type": "string" }, diff --git a/gen/jsonschema/fora.v1.TransactionItem.schema.strict.json b/gen/jsonschema/fora.v1.TransactionItem.schema.strict.json index eeb4cef4..69bdad58 100644 --- a/gen/jsonschema/fora.v1.TransactionItem.schema.strict.json +++ b/gen/jsonschema/fora.v1.TransactionItem.schema.strict.json @@ -94,7 +94,7 @@ "type": "array" }, "scopes": { - "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n also require a verified delegation that grants these scopes before it\n presents the term. To a requester that presents none, it answers as if the\n term did not exist, as existence hiding requires (see Requester.scopes). An\n offer the Exchange presented is honoured until it expires.\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", + "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n instead gate a term behind a verified delegation (a subscription term is\n the usual case): then only the delegation chain's verified effective scopes\n are matched against these scopes, and Requester.scopes is ignored for that\n term. To a requester that presents no valid delegation, it answers as if\n the term did not exist, as existence hiding requires (see\n Requester.scopes). An offer the Exchange presented is honoured until it\n expires, and a gated offer expires no later than the chain's effective\n expiry (see Delegation).\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments (at least one), and there is NO implicit prefix match\n (a grant narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", "items": { "type": "string" }, @@ -238,7 +238,7 @@ "type": "string" }, "expires_at": { - "description": "When this offer expires (ISO 8601).", + "description": "When this offer expires (ISO 8601). An offer for a term gated behind a\n verified delegation MUST NOT expire later than that delegation chain's\n effective expiry, the earliest `exp` in the chain (see Delegation).", "format": "date-time", "type": "string" }, diff --git a/gen/jsonschema/fora.v1.TransactionRequest.schema.json b/gen/jsonschema/fora.v1.TransactionRequest.schema.json index df890e44..48a7a527 100644 --- a/gen/jsonschema/fora.v1.TransactionRequest.schema.json +++ b/gen/jsonschema/fora.v1.TransactionRequest.schema.json @@ -95,10 +95,10 @@ "type": "object" }, "fora.v1.Delegation": { - "description": "The token is an opaque, signed credential. The format is a JWT (token_format\n \"jwt\"); the field stays open for a future format. Its claims are the\n AUTHORITATIVE source of the grant; the\n plaintext fields below (scopes, expires_at, max_spend_cents, …) are a\n convenience mirror the Exchange MAY use for fast pre-filtering before it\n verifies the token.\n\n Holder binding (the load-bearing property). The grant is bound to a holder key\n via the RFC 7800 `cnf` confirmation claim — `cnf.jkt`, the RFC 7638 JWK\n thumbprint of the holder's key. Verification MUST check that the key signing\n the request (RFC 9421) hashes to that thumbprint; a token possessed without\n the matching private key is rejected. This is what makes a leaked token NOT\n bearer-usable. One exception, the relayed purchase: when a Broker re-packages\n a purchase (BrokerService.ExecuteTransaction), the request an Exchange\n receives is signed by the Broker, not by the holder. There the Exchange checks\n the holder binding against the key each item's AgentAcceptance verifies under\n — the agent's key, resolved from the requester's WBA directory — and not\n against the request signer. A relayed purchase that carries a delegation\n therefore needs an AgentAcceptance on every item; an item without one fails\n the binding (DENIAL_REASON_DELEGATION_INVALID). On a query a Broker\n originated (Broker-led discovery), the Broker signs alone, so a delegation on\n it cannot pass holder binding: the Exchange does not honour it and answers\n as if none were present. How an agent proves a holder-of-key entitlement at\n discovery time through a Broker is an open protocol question. An Exchange\n that requires a verified delegation before it presents a scoped term (see\n LicenseTerm.scopes) therefore presents no such term on that query.\n\n Delegation is a chain of cnf-linked JWTs: a principal narrows a\n grant by issuing a child JWT (cnf = the next holder, scopes ⊆ parent), signed\n by the key the parent's cnf named — the chain-linkage invariant. Verifiers\n need only the root issuer's public key; intermediate keys ride inside the\n chain (JOSE header `jwk`), so verification is offline.\n\n The claim schema is a small registered vocabulary — see the FORA\n delegation-claims profile in the auth spec. All claims are OPTIONAL except the\n holder binding (cnf). Vendors MAY add namespaced claims (their own \"vendor:\"\n namespace; \"fora_\"-prefixed names are reserved for the registered vocabulary\n and MUST NOT be redefined); any constraint a verifier cannot evaluate is\n binding by default (fail closed) unless the issuer marks it advisory —\n mirroring Restriction.advisory.\n\n Scope/time/spend caps are defense-in-depth that bound the blast radius only in\n the residual case where the holder's signing key is also compromised; the\n primary protection against theft is the holder binding above.", + "description": "The token is a signed JWT (token_format \"jwt\"; the field stays open for a\n future format) carrying a holder-bound delegation chain. Its claims are the\n AUTHORITATIVE source of the grant; the plaintext fields below (scopes,\n expires_at, max_spend_cents, …) are a mirror the Exchange MAY use for fast\n pre-filtering before it verifies the token. A mirror establishes nothing: it\n cannot grant a scope, extend validity or raise a cap beyond what the signed\n chain says.\n\n The profile — chain encoding and limits, protected header, claims, clock\n leeway, issuer, audience and holder-binding rules, the verification\n procedure and the outcome of each failure — is the \"Delegation\" section of\n the authentication page and is not restated here; the Go SDK verifier\n (sdk/go/helpers) and the shared chain cases are derived from it. What a\n field reader must know: the grant is bound to a holder key by `cnf.jkt`, and\n the key a request proves possession of MUST hash to the terminal token's\n `cnf.jkt` (the RFC 9421 request signer on a direct call; on a purchase a\n Broker relayed, the key each item's AgentAcceptance verifies under), so a\n token possessed without its private key grants nothing. On a query a Broker\n originated nothing can pass that binding: the Exchange does not honour the\n delegation, answers as if none were present, and presents no term gated\n behind one (see LicenseTerm.scopes).", "properties": { "expires_at": { - "description": "When this delegation expires. Exchange MUST reject expired tokens.", + "description": "Mirror of the chain's earliest `exp`. The Exchange MUST reject an expired\n token; the signed `exp` values decide, not this field.", "format": "date-time", "type": "string" }, @@ -114,7 +114,7 @@ "type": "array" }, "issuer": { - "description": "Token issuer. OIDC issuer URL or GNAP grant server URL.\n Exchange uses this for JWT validation (OIDC discovery → JWKS)\n or GNAP token introspection.", + "description": "Mirror of the ROOT token's `iss`, the identity configured with the trusted\n key (never an intermediate or terminal issuer). Informational: the\n Exchange never discovers a key from it, through OIDC, JWKS or otherwise.", "type": "string" }, "max_accesses": { @@ -137,12 +137,12 @@ }, "principal_domain": { "default": "", - "description": "Who granted this delegation (domain for public key lookup).", + "description": "Mirror of the ROOT token's `iss`, the configured trust anchor's identity.\n Informational: no key is looked up from it, and it establishes no trust.", "type": "string" }, "principal_id": { "default": "", - "description": "Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").", + "description": "Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").\n Informational; not read by the verifier.", "type": "string" }, "quota_period": { @@ -151,11 +151,11 @@ "type": "string" }, "revocation_uri": { - "description": "Optional: URI for real-time revocation checking.\n Exchange MAY check this for high-value transactions.\n Not checked for routine low-value access (performance tradeoff).", + "description": "Informational. Revocation lookup is not part of the delegation profile:\n the verifier never fetches this URI. A chain is invalidated early by\n withdrawing the configured root key.", "type": "string" }, "scopes": { - "description": "Scopes granted by this delegation. MUST be a subset of the\n principal's own scopes (attenuation — can only narrow, not widen).", + "description": "Mirror of the terminal token's effective scopes. In the chain, each\n child's scopes are covered by its parent's under the scope-matching rule\n on Requester.scopes (attenuation — can only narrow, not widen).", "items": { "type": "string" }, @@ -163,8 +163,9 @@ }, "token": { "default": null, - "description": "Token bytes. A JWT (base64url-encoded JWS).", - "pattern": "^[A-Za-z0-9+/]*={0,2}$", + "description": "Token bytes. A JWT (base64url-encoded JWS). At most 32768 bytes: the\n complete chain, every embedded parent included, must fit. The limit is a\n wire rule, checked before the token is parsed and independent of whether\n the bytes are a valid JWT; a longer token is an invalid request, not a\n per-item denial.", + "maxLength": 43692, + "pattern": "^[A-Za-z0-9+/]{0,43691}={0,2}$", "type": "string" }, "token_format": { @@ -247,7 +248,7 @@ "type": "array" }, "scopes": { - "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n also require a verified delegation that grants these scopes before it\n presents the term. To a requester that presents none, it answers as if the\n term did not exist, as existence hiding requires (see Requester.scopes). An\n offer the Exchange presented is honoured until it expires.\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", + "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n instead gate a term behind a verified delegation (a subscription term is\n the usual case): then only the delegation chain's verified effective scopes\n are matched against these scopes, and Requester.scopes is ignored for that\n term. To a requester that presents no valid delegation, it answers as if\n the term did not exist, as existence hiding requires (see\n Requester.scopes). An offer the Exchange presented is honoured until it\n expires, and a gated offer expires no later than the chain's effective\n expiry (see Delegation).\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments (at least one), and there is NO implicit prefix match\n (a grant narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", "items": { "type": "string" }, @@ -389,7 +390,7 @@ "type": "string" }, "expires_at": { - "description": "When this offer expires (ISO 8601).", + "description": "When this offer expires (ISO 8601). An offer for a term gated behind a\n verified delegation MUST NOT expire later than that delegation chain's\n effective expiry, the earliest `exp` in the chain (see Delegation).", "format": "date-time", "type": "string" }, @@ -732,7 +733,7 @@ "type": "string" }, "scopes": { - "description": "EXISTENCE HIDING. A requester never learns about a resource outside its\n scopes. When the scopes leave no presentable offer for a resource, the\n Exchange answers with no offers and no absence reason (an OfferGroup with\n empty `offers` and `absence_reason` unset), exactly as for any resource\n with nothing to offer. A Broker relaying that answer adds no reason of its\n own. At purchase, an offer the Exchange presented is honoured until it\n expires, so a scope refusal cannot arise there. No reason in the protocol\n reports a scope shortfall: OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT and\n DENIAL_REASON_SCOPE_INSUFFICIENT are deprecated and never sent.\n\n Scope format: colon-separated segments, \"{domain}:{permission}\" or\n \"{profile}:{permission}\", optionally multi-segment (\"dist:US:CA\");\n matching is segment-wise per the rule below (no implicit hierarchy).\n Examples:\n \"credit:read\" — can access credit reports\n \"subscription:marketdata-2026\" — has active MarketData subscription\n \"academic:*\" — full access to academic resources\n \"internal:reports\" — can access internal reports\n \"*\" — unrestricted (public Exchange default)\n\n Matching is SEGMENT-WISE (\":\" separated). A granted scope G covers a\n required scope R iff, segment by segment, each G segment equals the\n corresponding R segment or is \"*\"; a terminal \"*\" matches all remaining\n segments. There is NO implicit prefix match, and a grant NARROWER than\n the requirement does not cover it (G must be equal-to-or-broader than R).\n Examples: \"dist:*\" covers \"dist:US\" and \"dist:US:CA\"; \"dist:US:*\" covers\n \"dist:US:CA\" but not \"dist:EU\"; bare \"dist\" covers only \"dist\"; granted\n \"dist:US:CA\" does NOT cover required \"dist:US\"; \"*\" covers everything.\n This same rule governs LicenseTerm.scopes — one algorithm protocol-wide.\n\n When empty, Exchange applies its default access policy (typically\n returns all publicly available resources).", + "description": "EXISTENCE HIDING. A requester never learns about a resource outside its\n scopes. When the scopes leave no presentable offer for a resource, the\n Exchange answers with no offers and no absence reason (an OfferGroup with\n empty `offers` and `absence_reason` unset), exactly as for any resource\n with nothing to offer. A Broker relaying that answer adds no reason of its\n own. At purchase, an offer the Exchange presented is honoured until it\n expires, so a scope refusal cannot arise there. No reason in the protocol\n reports a scope shortfall: OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT and\n DENIAL_REASON_SCOPE_INSUFFICIENT are deprecated and never sent.\n\n Scope format: colon-separated segments, \"{domain}:{permission}\" or\n \"{profile}:{permission}\", optionally multi-segment (\"dist:US:CA\");\n matching is segment-wise per the rule below (no implicit hierarchy).\n Examples:\n \"credit:read\" — can access credit reports\n \"subscription:marketdata-2026\" — has active MarketData subscription\n \"academic:*\" — full access to academic resources\n \"internal:reports\" — can access internal reports\n \"*\" — unrestricted (public Exchange default)\n\n Matching is SEGMENT-WISE (\":\" separated). A granted scope G covers a\n required scope R iff, segment by segment, each G segment equals the\n corresponding R segment or is \"*\"; a terminal \"*\" matches all remaining\n segments, of which there must be at least one. There is NO implicit prefix\n match, and a grant NARROWER than the requirement does not cover it (G must\n be equal-to-or-broader than R), so a grant with more segments than the\n requirement never covers it, even when its last segment is \"*\". A \"*\" in R\n is a literal segment: a narrow grant never covers a wildcard requirement.\n Matching is case-sensitive and never trims. Examples: \"dist:*\" covers\n \"dist:US\", \"dist:US:CA\" and \"dist:US:*\" but not \"dist\"; \"dist:US:*\" covers\n \"dist:US:CA\" but not \"dist:EU\" or \"dist:US\"; \"publisher:*:read\" covers\n \"publisher:news:read\" but not \"publisher:news:*\"; bare \"dist\" covers only\n \"dist\"; granted \"dist:US:CA\" does NOT cover required \"dist:US\"; \"*\" covers\n everything. This same rule governs LicenseTerm.scopes and the attenuation\n of a delegation child against its parent — one algorithm protocol-wide.\n\n When empty, Exchange applies its default access policy (typically\n returns all publicly available resources).", "items": { "type": "string" }, diff --git a/gen/jsonschema/fora.v1.TransactionRequest.schema.strict.json b/gen/jsonschema/fora.v1.TransactionRequest.schema.strict.json index c44be5fa..e63e526e 100644 --- a/gen/jsonschema/fora.v1.TransactionRequest.schema.strict.json +++ b/gen/jsonschema/fora.v1.TransactionRequest.schema.strict.json @@ -100,10 +100,10 @@ }, "fora.v1.Delegation": { "additionalProperties": false, - "description": "The token is an opaque, signed credential. The format is a JWT (token_format\n \"jwt\"); the field stays open for a future format. Its claims are the\n AUTHORITATIVE source of the grant; the\n plaintext fields below (scopes, expires_at, max_spend_cents, …) are a\n convenience mirror the Exchange MAY use for fast pre-filtering before it\n verifies the token.\n\n Holder binding (the load-bearing property). The grant is bound to a holder key\n via the RFC 7800 `cnf` confirmation claim — `cnf.jkt`, the RFC 7638 JWK\n thumbprint of the holder's key. Verification MUST check that the key signing\n the request (RFC 9421) hashes to that thumbprint; a token possessed without\n the matching private key is rejected. This is what makes a leaked token NOT\n bearer-usable. One exception, the relayed purchase: when a Broker re-packages\n a purchase (BrokerService.ExecuteTransaction), the request an Exchange\n receives is signed by the Broker, not by the holder. There the Exchange checks\n the holder binding against the key each item's AgentAcceptance verifies under\n — the agent's key, resolved from the requester's WBA directory — and not\n against the request signer. A relayed purchase that carries a delegation\n therefore needs an AgentAcceptance on every item; an item without one fails\n the binding (DENIAL_REASON_DELEGATION_INVALID). On a query a Broker\n originated (Broker-led discovery), the Broker signs alone, so a delegation on\n it cannot pass holder binding: the Exchange does not honour it and answers\n as if none were present. How an agent proves a holder-of-key entitlement at\n discovery time through a Broker is an open protocol question. An Exchange\n that requires a verified delegation before it presents a scoped term (see\n LicenseTerm.scopes) therefore presents no such term on that query.\n\n Delegation is a chain of cnf-linked JWTs: a principal narrows a\n grant by issuing a child JWT (cnf = the next holder, scopes ⊆ parent), signed\n by the key the parent's cnf named — the chain-linkage invariant. Verifiers\n need only the root issuer's public key; intermediate keys ride inside the\n chain (JOSE header `jwk`), so verification is offline.\n\n The claim schema is a small registered vocabulary — see the FORA\n delegation-claims profile in the auth spec. All claims are OPTIONAL except the\n holder binding (cnf). Vendors MAY add namespaced claims (their own \"vendor:\"\n namespace; \"fora_\"-prefixed names are reserved for the registered vocabulary\n and MUST NOT be redefined); any constraint a verifier cannot evaluate is\n binding by default (fail closed) unless the issuer marks it advisory —\n mirroring Restriction.advisory.\n\n Scope/time/spend caps are defense-in-depth that bound the blast radius only in\n the residual case where the holder's signing key is also compromised; the\n primary protection against theft is the holder binding above.", + "description": "The token is a signed JWT (token_format \"jwt\"; the field stays open for a\n future format) carrying a holder-bound delegation chain. Its claims are the\n AUTHORITATIVE source of the grant; the plaintext fields below (scopes,\n expires_at, max_spend_cents, …) are a mirror the Exchange MAY use for fast\n pre-filtering before it verifies the token. A mirror establishes nothing: it\n cannot grant a scope, extend validity or raise a cap beyond what the signed\n chain says.\n\n The profile — chain encoding and limits, protected header, claims, clock\n leeway, issuer, audience and holder-binding rules, the verification\n procedure and the outcome of each failure — is the \"Delegation\" section of\n the authentication page and is not restated here; the Go SDK verifier\n (sdk/go/helpers) and the shared chain cases are derived from it. What a\n field reader must know: the grant is bound to a holder key by `cnf.jkt`, and\n the key a request proves possession of MUST hash to the terminal token's\n `cnf.jkt` (the RFC 9421 request signer on a direct call; on a purchase a\n Broker relayed, the key each item's AgentAcceptance verifies under), so a\n token possessed without its private key grants nothing. On a query a Broker\n originated nothing can pass that binding: the Exchange does not honour the\n delegation, answers as if none were present, and presents no term gated\n behind one (see LicenseTerm.scopes).", "properties": { "expires_at": { - "description": "When this delegation expires. Exchange MUST reject expired tokens.", + "description": "Mirror of the chain's earliest `exp`. The Exchange MUST reject an expired\n token; the signed `exp` values decide, not this field.", "format": "date-time", "type": "string" }, @@ -119,7 +119,7 @@ "type": "array" }, "issuer": { - "description": "Token issuer. OIDC issuer URL or GNAP grant server URL.\n Exchange uses this for JWT validation (OIDC discovery → JWKS)\n or GNAP token introspection.", + "description": "Mirror of the ROOT token's `iss`, the identity configured with the trusted\n key (never an intermediate or terminal issuer). Informational: the\n Exchange never discovers a key from it, through OIDC, JWKS or otherwise.", "type": "string" }, "max_accesses": { @@ -142,12 +142,12 @@ }, "principal_domain": { "default": "", - "description": "Who granted this delegation (domain for public key lookup).", + "description": "Mirror of the ROOT token's `iss`, the configured trust anchor's identity.\n Informational: no key is looked up from it, and it establishes no trust.", "type": "string" }, "principal_id": { "default": "", - "description": "Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").", + "description": "Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").\n Informational; not read by the verifier.", "type": "string" }, "quota_period": { @@ -156,11 +156,11 @@ "type": "string" }, "revocation_uri": { - "description": "Optional: URI for real-time revocation checking.\n Exchange MAY check this for high-value transactions.\n Not checked for routine low-value access (performance tradeoff).", + "description": "Informational. Revocation lookup is not part of the delegation profile:\n the verifier never fetches this URI. A chain is invalidated early by\n withdrawing the configured root key.", "type": "string" }, "scopes": { - "description": "Scopes granted by this delegation. MUST be a subset of the\n principal's own scopes (attenuation — can only narrow, not widen).", + "description": "Mirror of the terminal token's effective scopes. In the chain, each\n child's scopes are covered by its parent's under the scope-matching rule\n on Requester.scopes (attenuation — can only narrow, not widen).", "items": { "type": "string" }, @@ -168,8 +168,9 @@ }, "token": { "default": null, - "description": "Token bytes. A JWT (base64url-encoded JWS).", - "pattern": "^[A-Za-z0-9+/]*={0,2}$", + "description": "Token bytes. A JWT (base64url-encoded JWS). At most 32768 bytes: the\n complete chain, every embedded parent included, must fit. The limit is a\n wire rule, checked before the token is parsed and independent of whether\n the bytes are a valid JWT; a longer token is an invalid request, not a\n per-item denial.", + "maxLength": 43692, + "pattern": "^[A-Za-z0-9+/]{0,43691}={0,2}$", "type": "string" }, "token_format": { @@ -254,7 +255,7 @@ "type": "array" }, "scopes": { - "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n also require a verified delegation that grants these scopes before it\n presents the term. To a requester that presents none, it answers as if the\n term did not exist, as existence hiding requires (see Requester.scopes). An\n offer the Exchange presented is honoured until it expires.\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", + "description": "Requester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n instead gate a term behind a verified delegation (a subscription term is\n the usual case): then only the delegation chain's verified effective scopes\n are matched against these scopes, and Requester.scopes is ignored for that\n term. To a requester that presents no valid delegation, it answers as if\n the term did not exist, as existence hiding requires (see\n Requester.scopes). An offer the Exchange presented is honoured until it\n expires, and a gated offer expires no later than the chain's effective\n expiry (see Delegation).\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments (at least one), and there is NO implicit prefix match\n (a grant narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.", "items": { "type": "string" }, @@ -398,7 +399,7 @@ "type": "string" }, "expires_at": { - "description": "When this offer expires (ISO 8601).", + "description": "When this offer expires (ISO 8601). An offer for a term gated behind a\n verified delegation MUST NOT expire later than that delegation chain's\n effective expiry, the earliest `exp` in the chain (see Delegation).", "format": "date-time", "type": "string" }, @@ -746,7 +747,7 @@ "type": "string" }, "scopes": { - "description": "EXISTENCE HIDING. A requester never learns about a resource outside its\n scopes. When the scopes leave no presentable offer for a resource, the\n Exchange answers with no offers and no absence reason (an OfferGroup with\n empty `offers` and `absence_reason` unset), exactly as for any resource\n with nothing to offer. A Broker relaying that answer adds no reason of its\n own. At purchase, an offer the Exchange presented is honoured until it\n expires, so a scope refusal cannot arise there. No reason in the protocol\n reports a scope shortfall: OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT and\n DENIAL_REASON_SCOPE_INSUFFICIENT are deprecated and never sent.\n\n Scope format: colon-separated segments, \"{domain}:{permission}\" or\n \"{profile}:{permission}\", optionally multi-segment (\"dist:US:CA\");\n matching is segment-wise per the rule below (no implicit hierarchy).\n Examples:\n \"credit:read\" — can access credit reports\n \"subscription:marketdata-2026\" — has active MarketData subscription\n \"academic:*\" — full access to academic resources\n \"internal:reports\" — can access internal reports\n \"*\" — unrestricted (public Exchange default)\n\n Matching is SEGMENT-WISE (\":\" separated). A granted scope G covers a\n required scope R iff, segment by segment, each G segment equals the\n corresponding R segment or is \"*\"; a terminal \"*\" matches all remaining\n segments. There is NO implicit prefix match, and a grant NARROWER than\n the requirement does not cover it (G must be equal-to-or-broader than R).\n Examples: \"dist:*\" covers \"dist:US\" and \"dist:US:CA\"; \"dist:US:*\" covers\n \"dist:US:CA\" but not \"dist:EU\"; bare \"dist\" covers only \"dist\"; granted\n \"dist:US:CA\" does NOT cover required \"dist:US\"; \"*\" covers everything.\n This same rule governs LicenseTerm.scopes — one algorithm protocol-wide.\n\n When empty, Exchange applies its default access policy (typically\n returns all publicly available resources).", + "description": "EXISTENCE HIDING. A requester never learns about a resource outside its\n scopes. When the scopes leave no presentable offer for a resource, the\n Exchange answers with no offers and no absence reason (an OfferGroup with\n empty `offers` and `absence_reason` unset), exactly as for any resource\n with nothing to offer. A Broker relaying that answer adds no reason of its\n own. At purchase, an offer the Exchange presented is honoured until it\n expires, so a scope refusal cannot arise there. No reason in the protocol\n reports a scope shortfall: OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT and\n DENIAL_REASON_SCOPE_INSUFFICIENT are deprecated and never sent.\n\n Scope format: colon-separated segments, \"{domain}:{permission}\" or\n \"{profile}:{permission}\", optionally multi-segment (\"dist:US:CA\");\n matching is segment-wise per the rule below (no implicit hierarchy).\n Examples:\n \"credit:read\" — can access credit reports\n \"subscription:marketdata-2026\" — has active MarketData subscription\n \"academic:*\" — full access to academic resources\n \"internal:reports\" — can access internal reports\n \"*\" — unrestricted (public Exchange default)\n\n Matching is SEGMENT-WISE (\":\" separated). A granted scope G covers a\n required scope R iff, segment by segment, each G segment equals the\n corresponding R segment or is \"*\"; a terminal \"*\" matches all remaining\n segments, of which there must be at least one. There is NO implicit prefix\n match, and a grant NARROWER than the requirement does not cover it (G must\n be equal-to-or-broader than R), so a grant with more segments than the\n requirement never covers it, even when its last segment is \"*\". A \"*\" in R\n is a literal segment: a narrow grant never covers a wildcard requirement.\n Matching is case-sensitive and never trims. Examples: \"dist:*\" covers\n \"dist:US\", \"dist:US:CA\" and \"dist:US:*\" but not \"dist\"; \"dist:US:*\" covers\n \"dist:US:CA\" but not \"dist:EU\" or \"dist:US\"; \"publisher:*:read\" covers\n \"publisher:news:read\" but not \"publisher:news:*\"; bare \"dist\" covers only\n \"dist\"; granted \"dist:US:CA\" does NOT cover required \"dist:US\"; \"*\" covers\n everything. This same rule governs LicenseTerm.scopes and the attenuation\n of a delegation child against its parent — one algorithm protocol-wide.\n\n When empty, Exchange applies its default access policy (typically\n returns all publicly available resources).", "items": { "type": "string" }, diff --git a/gen/python/wire/models.py b/gen/python/wire/models.py index 854fe27d..b47e9b46 100644 --- a/gen/python/wire/models.py +++ b/gen/python/wire/models.py @@ -146,7 +146,7 @@ class Cost(WireModel): class Delegation(WireModel): expires_at: AwareDatetime | None = Field( None, - description='When this delegation expires. Exchange MUST reject expired tokens.', + description="Mirror of the chain's earliest `exp`. The Exchange MUST reject an expired\n token; the signed `exp` values decide, not this field.", ) ext: dict[str, Any] | None = Field(None, description='Extension point') ext_critical: list[str] | None = Field( @@ -155,7 +155,7 @@ class Delegation(WireModel): ) issuer: str | None = Field( None, - description='Token issuer. OIDC issuer URL or GNAP grant server URL.\n Exchange uses this for JWT validation (OIDC discovery → JWKS)\n or GNAP token introspection.', + description="Mirror of the ROOT token's `iss`, the identity configured with the trusted\n key (never an intermediate or terminal issuer). Informational: the\n Exchange never discovers a key from it, through OIDC, JWKS or otherwise.", ) max_accesses: conint(ge=-2147483648, le=2147483647) | None = Field( None, @@ -166,11 +166,12 @@ class Delegation(WireModel): description='Maximum spend in currency minor units (e.g., cents for USD).\n Exchange tracks cumulative spend against this cap.', ) principal_domain: str | None = Field( - '', description='Who granted this delegation (domain for public key lookup).' + '', + description="Mirror of the ROOT token's `iss`, the configured trust anchor's identity.\n Informational: no key is looked up from it, and it establishes no trust.", ) principal_id: str | None = Field( '', - description='Principal\'s identifier (e.g., "user@acme.com", "marketdata.example.com").', + description='Principal\'s identifier (e.g., "user@acme.com", "marketdata.example.com").\n Informational; not read by the verifier.', ) quota_period: str | None = Field( None, @@ -178,14 +179,17 @@ class Delegation(WireModel): ) revocation_uri: str | None = Field( None, - description='Optional: URI for real-time revocation checking.\n Exchange MAY check this for high-value transactions.\n Not checked for routine low-value access (performance tradeoff).', + description='Informational. Revocation lookup is not part of the delegation profile:\n the verifier never fetches this URI. A chain is invalidated early by\n withdrawing the configured root key.', ) scopes: list[str] | None = Field( None, - description="Scopes granted by this delegation. MUST be a subset of the\n principal's own scopes (attenuation — can only narrow, not widen).", + description="Mirror of the terminal token's effective scopes. In the chain, each\n child's scopes are covered by its parent's under the scope-matching rule\n on Requester.scopes (attenuation — can only narrow, not widen).", ) - token: constr(pattern=r'^[A-Za-z0-9+/]*={0,2}$') | None = Field( - '', description='Token bytes. A JWT (base64url-encoded JWS).' + token: ( + constr(pattern=r'^[A-Za-z0-9+/]{0,43691}={0,2}$', max_length=43692) | None + ) = Field( + '', + description='Token bytes. A JWT (base64url-encoded JWS). At most 32768 bytes: the\n complete chain, every embedded parent included, must fit. The limit is a\n wire rule, checked before the token is parsed and independent of whether\n the bytes are a valid JWT; a longer token is an invalid request, not a\n per-item denial.', ) token_format: str | None = Field( '', @@ -1438,7 +1442,7 @@ class Requester(WireModel): ) scopes: list[str] | None = Field( None, - description='Entitlement scopes: the requester\'s own statement of what it is entitled\n to. On a query a Broker originated, they are the Broker\'s statement for the\n agent. They are signed and attributable, and the Exchange does not verify\n them. Scope coverage decides which terms the Exchange presents (see\n LicenseTerm.scopes, which also states when an Exchange MAY require a\n verified delegation); it is not access control. A false statement is\n traceable to the signing key, its directory and its account, and the\n agent\'s acceptance signs the scoped offer it buys; it is settled through\n reconciliation and the account relationship.\n\nEXISTENCE HIDING. A requester never learns about a resource outside its\n scopes. When the scopes leave no presentable offer for a resource, the\n Exchange answers with no offers and no absence reason (an OfferGroup with\n empty `offers` and `absence_reason` unset), exactly as for any resource\n with nothing to offer. A Broker relaying that answer adds no reason of its\n own. At purchase, an offer the Exchange presented is honoured until it\n expires, so a scope refusal cannot arise there. No reason in the protocol\n reports a scope shortfall: OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT and\n DENIAL_REASON_SCOPE_INSUFFICIENT are deprecated and never sent.\n\n Scope format: colon-separated segments, "{domain}:{permission}" or\n "{profile}:{permission}", optionally multi-segment ("dist:US:CA");\n matching is segment-wise per the rule below (no implicit hierarchy).\n Examples:\n "credit:read" — can access credit reports\n "subscription:marketdata-2026" — has active MarketData subscription\n "academic:*" — full access to academic resources\n "internal:reports" — can access internal reports\n "*" — unrestricted (public Exchange default)\n\n Matching is SEGMENT-WISE (":" separated). A granted scope G covers a\n required scope R iff, segment by segment, each G segment equals the\n corresponding R segment or is "*"; a terminal "*" matches all remaining\n segments. There is NO implicit prefix match, and a grant NARROWER than\n the requirement does not cover it (G must be equal-to-or-broader than R).\n Examples: "dist:*" covers "dist:US" and "dist:US:CA"; "dist:US:*" covers\n "dist:US:CA" but not "dist:EU"; bare "dist" covers only "dist"; granted\n "dist:US:CA" does NOT cover required "dist:US"; "*" covers everything.\n This same rule governs LicenseTerm.scopes — one algorithm protocol-wide.\n\n When empty, Exchange applies its default access policy (typically\n returns all publicly available resources).', + description='Entitlement scopes: the requester\'s own statement of what it is entitled\n to. On a query a Broker originated, they are the Broker\'s statement for the\n agent. They are signed and attributable, and the Exchange does not verify\n them. Scope coverage decides which terms the Exchange presents (see\n LicenseTerm.scopes, which also states when an Exchange MAY require a\n verified delegation); it is not access control. A false statement is\n traceable to the signing key, its directory and its account, and the\n agent\'s acceptance signs the scoped offer it buys; it is settled through\n reconciliation and the account relationship.\n\nEXISTENCE HIDING. A requester never learns about a resource outside its\n scopes. When the scopes leave no presentable offer for a resource, the\n Exchange answers with no offers and no absence reason (an OfferGroup with\n empty `offers` and `absence_reason` unset), exactly as for any resource\n with nothing to offer. A Broker relaying that answer adds no reason of its\n own. At purchase, an offer the Exchange presented is honoured until it\n expires, so a scope refusal cannot arise there. No reason in the protocol\n reports a scope shortfall: OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT and\n DENIAL_REASON_SCOPE_INSUFFICIENT are deprecated and never sent.\n\n Scope format: colon-separated segments, "{domain}:{permission}" or\n "{profile}:{permission}", optionally multi-segment ("dist:US:CA");\n matching is segment-wise per the rule below (no implicit hierarchy).\n Examples:\n "credit:read" — can access credit reports\n "subscription:marketdata-2026" — has active MarketData subscription\n "academic:*" — full access to academic resources\n "internal:reports" — can access internal reports\n "*" — unrestricted (public Exchange default)\n\n Matching is SEGMENT-WISE (":" separated). A granted scope G covers a\n required scope R iff, segment by segment, each G segment equals the\n corresponding R segment or is "*"; a terminal "*" matches all remaining\n segments, of which there must be at least one. There is NO implicit prefix\n match, and a grant NARROWER than the requirement does not cover it (G must\n be equal-to-or-broader than R), so a grant with more segments than the\n requirement never covers it, even when its last segment is "*". A "*" in R\n is a literal segment: a narrow grant never covers a wildcard requirement.\n Matching is case-sensitive and never trims. Examples: "dist:*" covers\n "dist:US", "dist:US:CA" and "dist:US:*" but not "dist"; "dist:US:*" covers\n "dist:US:CA" but not "dist:EU" or "dist:US"; "publisher:*:read" covers\n "publisher:news:read" but not "publisher:news:*"; bare "dist" covers only\n "dist"; granted "dist:US:CA" does NOT cover required "dist:US"; "*" covers\n everything. This same rule governs LicenseTerm.scopes and the attenuation\n of a delegation child against its parent — one algorithm protocol-wide.\n\n When empty, Exchange applies its default access policy (typically\n returns all publicly available resources).', max_length=64, ) type: RequesterType = Field( @@ -1882,7 +1886,7 @@ class LicenseTerm(WireModel): ) scopes: list[str] | None = Field( None, - description='Scope-gating: the Exchange presents an offer for this term only when the\n scopes in the requester\'s signed request (Requester.scopes) cover ALL of\n these scopes (AND-semantics). Empty = public. A subscription term is\n Pricing{model:FREE} + scopes:["subscription:..."].\n\nRequester.scopes is the requester\'s own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n also require a verified delegation that grants these scopes before it\n presents the term. To a requester that presents none, it answers as if the\n term did not exist, as existence hiding requires (see Requester.scopes). An\n offer the Exchange presented is honoured until it expires.\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (":" separated), each granted segment must equal the\n corresponding required segment or be "*", a terminal "*" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). "dist:*" covers\n "dist:US" and "dist:US:CA"; "dist" covers only "dist". There is exactly\n one scope-matching algorithm across the protocol.', + description='Scope-gating: the Exchange presents an offer for this term only when the\n scopes in the requester\'s signed request (Requester.scopes) cover ALL of\n these scopes (AND-semantics). Empty = public. A subscription term is\n Pricing{model:FREE} + scopes:["subscription:..."].\n\nRequester.scopes is the requester\'s own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n instead gate a term behind a verified delegation (a subscription term is\n the usual case): then only the delegation chain\'s verified effective scopes\n are matched against these scopes, and Requester.scopes is ignored for that\n term. To a requester that presents no valid delegation, it answers as if\n the term did not exist, as existence hiding requires (see\n Requester.scopes). An offer the Exchange presented is honoured until it\n expires, and a gated offer expires no later than the chain\'s effective\n expiry (see Delegation).\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (":" separated), each granted segment must equal the\n corresponding required segment or be "*", a terminal "*" matches all\n remaining segments (at least one), and there is NO implicit prefix match\n (a grant narrower than the requirement does not cover it). "dist:*" covers\n "dist:US" and "dist:US:CA"; "dist" covers only "dist". There is exactly\n one scope-matching algorithm across the protocol.', max_length=64, ) semantics: TermSemantics = Field( @@ -1913,7 +1917,8 @@ class Offer(WireModel): description='REQUIRED. Bare host of the Exchange that issued this offer (e.g.\n "exchange.example" or "exchange.example:8081"), in the form "Request\n recipient" defines in the file header. This is the execute-routing target:\n the agent, or a relaying Broker, sends the ExecuteTransaction call for this\n offer to this Exchange, and a Broker relaying a mixed batch groups the items\n by this value. Because it is an ordinary Offer field it falls inside the\n signed bytes (see `signature` below — the signature covers every field\n except `signature` / `signature_algorithm`), so an intermediary cannot\n redirect the execute call to a different Exchange without invalidating the\n offer, and it is what retires the X-FORA-Exchange-Endpoint transport header.\n It is also the audience statement of an ExecuteTransaction, which is why\n TransactionRequest carries no top-level `exchange`: on receipt, an Exchange\n MUST reject the request unless EVERY item\'s offer.exchange names its own\n domain. Presence is enforced because an empty value is unroutable — a\n relaying Broker has nothing to group or dial on, and the swap-protection\n above is vacuous when the signed bytes carry no recipient at all.', ) expires_at: AwareDatetime | None = Field( - None, description='When this offer expires (ISO 8601).' + None, + description="When this offer expires (ISO 8601). An offer for a term gated behind a\n verified delegation MUST NOT expire later than that delegation chain's\n effective expiry, the earliest `exp` in the chain (see Delegation).", ) ext: dict[str, Any] | None = Field(None, description='Extension point') ext_critical: list[str] | None = Field( diff --git a/gen/ts/wire/schemas.ts b/gen/ts/wire/schemas.ts index 55629e22..1646c88b 100644 --- a/gen/ts/wire/schemas.ts +++ b/gen/ts/wire/schemas.ts @@ -40,7 +40,7 @@ export const CitationFormatSchema = /* @__PURE__ */ (() => wire(z.enum(["CITATIO export const CostSchema = /* @__PURE__ */ (() => wire(z.object({ "amount": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Exact decimal string (not a float), e.g. \"19.99\". Denominated in `currency`.").default(""), "currency": z.string().default(""), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).optional() }).describe("Cost — Actual transaction cost.")))(); -export const DelegationSchema = /* @__PURE__ */ (() => wire(z.object({ "expires_at": z.string().datetime({ offset: true }).describe("When this delegation expires. Exchange MUST reject expired tokens.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "issuer": z.string().describe("Token issuer. OIDC issuer URL or GNAP grant server URL.\n Exchange uses this for JWT validation (OIDC discovery → JWKS)\n or GNAP token introspection.").optional(), "max_accesses": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Maximum number of accesses allowed under this delegation.\n Exchange tracks cumulative access count against this cap.\n Deny with DENIAL_REASON_QUOTA_EXCEEDED when count >= limit.\n For subscriptions with \"10,000 accesses/month\", this carries the ceiling.").optional(), "max_spend_cents": z.coerce.number().int().describe("Maximum spend in currency minor units (e.g., cents for USD).\n Exchange tracks cumulative spend against this cap.").optional(), "principal_domain": z.string().describe("Who granted this delegation (domain for public key lookup).").default(""), "principal_id": z.string().describe("Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").").default(""), "quota_period": z.string().describe("Quota reset period. How often the access/spend counters reset.\n Example: 30 days for monthly subscriptions — \"2592000s\" on the wire\n (proto-JSON encodes Duration as seconds; \"720h\" is not accepted).\n When absent, the quota is lifetime (bounded only by expires_at).").optional(), "revocation_uri": z.string().describe("Optional: URI for real-time revocation checking.\n Exchange MAY check this for high-value transactions.\n Not checked for routine low-value access (performance tradeoff).").optional(), "scopes": z.array(z.string()).describe("Scopes granted by this delegation. MUST be a subset of the\n principal's own scopes (attenuation — can only narrow, not widen).").optional(), "token": z.string().regex(new RegExp("^[A-Za-z0-9+/]*={0,2}$")).describe("Token bytes. A JWT (base64url-encoded JWS).").default(""), "token_format": z.string().describe("Token format: \"jwt\" (default). Empty is treated as \"jwt\". The field stays\n open for a future format.").default("") }).describe("Delegation — Scoped, time-limited, spend-capped credential.\n\nThe token is an opaque, signed credential. The format is a JWT (token_format\n \"jwt\"); the field stays open for a future format. Its claims are the\n AUTHORITATIVE source of the grant; the\n plaintext fields below (scopes, expires_at, max_spend_cents, …) are a\n convenience mirror the Exchange MAY use for fast pre-filtering before it\n verifies the token.\n\n Holder binding (the load-bearing property). The grant is bound to a holder key\n via the RFC 7800 `cnf` confirmation claim — `cnf.jkt`, the RFC 7638 JWK\n thumbprint of the holder's key. Verification MUST check that the key signing\n the request (RFC 9421) hashes to that thumbprint; a token possessed without\n the matching private key is rejected. This is what makes a leaked token NOT\n bearer-usable. One exception, the relayed purchase: when a Broker re-packages\n a purchase (BrokerService.ExecuteTransaction), the request an Exchange\n receives is signed by the Broker, not by the holder. There the Exchange checks\n the holder binding against the key each item's AgentAcceptance verifies under\n — the agent's key, resolved from the requester's WBA directory — and not\n against the request signer. A relayed purchase that carries a delegation\n therefore needs an AgentAcceptance on every item; an item without one fails\n the binding (DENIAL_REASON_DELEGATION_INVALID). On a query a Broker\n originated (Broker-led discovery), the Broker signs alone, so a delegation on\n it cannot pass holder binding: the Exchange does not honour it and answers\n as if none were present. How an agent proves a holder-of-key entitlement at\n discovery time through a Broker is an open protocol question. An Exchange\n that requires a verified delegation before it presents a scoped term (see\n LicenseTerm.scopes) therefore presents no such term on that query.\n\n Delegation is a chain of cnf-linked JWTs: a principal narrows a\n grant by issuing a child JWT (cnf = the next holder, scopes ⊆ parent), signed\n by the key the parent's cnf named — the chain-linkage invariant. Verifiers\n need only the root issuer's public key; intermediate keys ride inside the\n chain (JOSE header `jwk`), so verification is offline.\n\n The claim schema is a small registered vocabulary — see the FORA\n delegation-claims profile in the auth spec. All claims are OPTIONAL except the\n holder binding (cnf). Vendors MAY add namespaced claims (their own \"vendor:\"\n namespace; \"fora_\"-prefixed names are reserved for the registered vocabulary\n and MUST NOT be redefined); any constraint a verifier cannot evaluate is\n binding by default (fail closed) unless the issuer marks it advisory —\n mirroring Restriction.advisory.\n\n Scope/time/spend caps are defense-in-depth that bound the blast radius only in\n the residual case where the holder's signing key is also compromised; the\n primary protection against theft is the holder binding above.")))(); +export const DelegationSchema = /* @__PURE__ */ (() => wire(z.object({ "expires_at": z.string().datetime({ offset: true }).describe("Mirror of the chain's earliest `exp`. The Exchange MUST reject an expired\n token; the signed `exp` values decide, not this field.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "issuer": z.string().describe("Mirror of the ROOT token's `iss`, the identity configured with the trusted\n key (never an intermediate or terminal issuer). Informational: the\n Exchange never discovers a key from it, through OIDC, JWKS or otherwise.").optional(), "max_accesses": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Maximum number of accesses allowed under this delegation.\n Exchange tracks cumulative access count against this cap.\n Deny with DENIAL_REASON_QUOTA_EXCEEDED when count >= limit.\n For subscriptions with \"10,000 accesses/month\", this carries the ceiling.").optional(), "max_spend_cents": z.coerce.number().int().describe("Maximum spend in currency minor units (e.g., cents for USD).\n Exchange tracks cumulative spend against this cap.").optional(), "principal_domain": z.string().describe("Mirror of the ROOT token's `iss`, the configured trust anchor's identity.\n Informational: no key is looked up from it, and it establishes no trust.").default(""), "principal_id": z.string().describe("Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").\n Informational; not read by the verifier.").default(""), "quota_period": z.string().describe("Quota reset period. How often the access/spend counters reset.\n Example: 30 days for monthly subscriptions — \"2592000s\" on the wire\n (proto-JSON encodes Duration as seconds; \"720h\" is not accepted).\n When absent, the quota is lifetime (bounded only by expires_at).").optional(), "revocation_uri": z.string().describe("Informational. Revocation lookup is not part of the delegation profile:\n the verifier never fetches this URI. A chain is invalidated early by\n withdrawing the configured root key.").optional(), "scopes": z.array(z.string()).describe("Mirror of the terminal token's effective scopes. In the chain, each\n child's scopes are covered by its parent's under the scope-matching rule\n on Requester.scopes (attenuation — can only narrow, not widen).").optional(), "token": z.string().regex(new RegExp("^[A-Za-z0-9+/]{0,43691}={0,2}$")).max(43692).describe("Token bytes. A JWT (base64url-encoded JWS). At most 32768 bytes: the\n complete chain, every embedded parent included, must fit. The limit is a\n wire rule, checked before the token is parsed and independent of whether\n the bytes are a valid JWT; a longer token is an invalid request, not a\n per-item denial.").default(""), "token_format": z.string().describe("Token format: \"jwt\" (default). Empty is treated as \"jwt\". The field stays\n open for a future format.").default("") }).describe("Delegation — Scoped, time-limited, spend-capped credential.\n\nThe token is a signed JWT (token_format \"jwt\"; the field stays open for a\n future format) carrying a holder-bound delegation chain. Its claims are the\n AUTHORITATIVE source of the grant; the plaintext fields below (scopes,\n expires_at, max_spend_cents, …) are a mirror the Exchange MAY use for fast\n pre-filtering before it verifies the token. A mirror establishes nothing: it\n cannot grant a scope, extend validity or raise a cap beyond what the signed\n chain says.\n\n The profile — chain encoding and limits, protected header, claims, clock\n leeway, issuer, audience and holder-binding rules, the verification\n procedure and the outcome of each failure — is the \"Delegation\" section of\n the authentication page and is not restated here; the Go SDK verifier\n (sdk/go/helpers) and the shared chain cases are derived from it. What a\n field reader must know: the grant is bound to a holder key by `cnf.jkt`, and\n the key a request proves possession of MUST hash to the terminal token's\n `cnf.jkt` (the RFC 9421 request signer on a direct call; on a purchase a\n Broker relayed, the key each item's AgentAcceptance verifies under), so a\n token possessed without its private key grants nothing. On a query a Broker\n originated nothing can pass that binding: the Exchange does not honour the\n delegation, answers as if none were present, and presents no term gated\n behind one (see LicenseTerm.scopes).")))(); export const DeliveryMethodSchema = /* @__PURE__ */ (() => wire(z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"])))(); @@ -48,9 +48,9 @@ export const DenialReasonSchema = /* @__PURE__ */ (() => wire(z.enum(["DENIAL_RE export const DiscoveryMethodSchema = /* @__PURE__ */ (() => wire(z.enum(["DISCOVERY_METHOD_EXCHANGE","DISCOVERY_METHOD_SEARCH","DISCOVERY_METHOD_RECOMMENDATION","DISCOVERY_METHOD_SYNDICATION"])))(); -export const DiscoveryRequestSchema = /* @__PURE__ */ (() => wire(z.object({ "acceptable_restrictions": z.array(z.object({ "axis": z.union([z.string().regex(new RegExp("^RESTRICTION_KIND_UNSPECIFIED$")), z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("Which axis (same enum as Restriction.kind): FUNCTION / GEOGRAPHY /\n USER_TYPE / OTHER.").default(0), "values": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("The values the query operates within on this axis — same token vocabulary\n as the terms (e.g. FUNCTION [\"ai-train\"], GEOGRAPHY [\"US\", \"EU\"]).").optional() }).describe("AcceptableRestriction — the limits a query operates within on one restriction\n axis, expressed in the same RestrictionKind vocabulary that terms use. The\n Exchange/Broker MAY pre-select offers whose term restrictions fall within\n these as a convenience (see Restriction); it is NOT enforcement — the agent\n self-selects and bears compliance.")).describe("The limits the agent will operate within, per restriction axis — see\n AcceptableRestriction. The Broker forwards these to Exchanges in\n ResourceQuery.acceptable_restrictions. Advisory selection inputs, not\n enforcement.").optional(), "constraints": z.object({ "budget_period": z.string().describe("Budget period (e.g. \"2592000s\" = 30 days; proto-JSON encodes Duration\n as seconds).").optional(), "budget_scope": z.string().describe("Budget scope identifier: whose budget this is.\n E.g. \"user:u-12345\" for per-user budgets, \"team:eng\" for per-team.").optional(), "delivery_preference": z.array(z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"])).describe("Preferred delivery methods, in order of preference.").optional(), "exchanges": z.array(z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260)).describe("Authorized Exchange domains, in the shape \"Request recipient\" defines in the\n file header. Broker queries only these. This is a FILTER over third parties,\n not an address — the recipient of the request carrying it is a separate\n question.").optional(), "max_data_age": z.string().describe("Maximum acceptable age of resource data. The Broker SHOULD\n exclude offers where (now - Offer.data_as_of) exceeds this duration.\n\nOnly relevant for DYNAMIC resources. Ignored for STATIC (content is\n immutable) and LIVE (content doesn't exist yet).\n\n Examples:\n 7 days — \"credit report updated within the last week\"\n 1 hour — \"stock snapshot from the last hour\"\n 30 days — \"drug interaction database updated this month\"").optional(), "max_hops": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Maximum number of hops the agent will allow its discovery to travel. It\n rides only on DiscoveryRequest, which terminates at the Broker, and a\n Broker originates its own queries rather than forwarding the agent's\n request, so no party receives or enforces it in this version: an Exchange\n never sees it, and an Exchange's own cap is\n WellKnownManifest.max_intermediary_hops. The field is kept for multi-hop\n Broker discovery, an open protocol question. Absent = no cap stated.").optional(), "max_price": z.object({ "amount": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Exact decimal string (not a float), e.g. \"19.99\". Denominated in `currency`.").default(""), "currency": z.string().default(""), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).optional() }).describe("Maximum price the agent is willing to pay.").optional(), "max_unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Maximum effective cost per unit, as an exact decimal string (not a float).").optional(), "period_budget": z.object({ "amount": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Exact decimal string (not a float), e.g. \"19.99\". Denominated in `currency`.").default(""), "currency": z.string().default(""), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).optional() }).describe("Per-period budget limit for the budget_scope.").optional(), "preferred_exchanges": z.array(z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260)).describe("Exchanges the agent has existing relationships with (subscriptions,\n contracts). The Broker SHOULD prefer these when resource is\n available — subscription resource has zero marginal cost.").optional(), "reporting_capable": z.boolean().describe("Whether the agent supports post-usage reporting.").optional() }).describe("Constraints for exchange filtering and offer selection.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "query": z.string().describe("Search query for Broker-side resource discovery.\n Used when the agent doesn't know specific URIs but wants the Broker\n to find matching resources across Exchanges.\n When present, the Broker interprets the query and discovers resources\n across Exchanges on the agent's behalf. Results returned as Offers\n in DiscoveryResponse, same as for specific URI requests.\n Can be used alongside uris (specific URIs + search in one request).").optional(), "requester": z.object({ "delegation": z.object({ "expires_at": z.string().datetime({ offset: true }).describe("When this delegation expires. Exchange MUST reject expired tokens.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "issuer": z.string().describe("Token issuer. OIDC issuer URL or GNAP grant server URL.\n Exchange uses this for JWT validation (OIDC discovery → JWKS)\n or GNAP token introspection.").optional(), "max_accesses": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Maximum number of accesses allowed under this delegation.\n Exchange tracks cumulative access count against this cap.\n Deny with DENIAL_REASON_QUOTA_EXCEEDED when count >= limit.\n For subscriptions with \"10,000 accesses/month\", this carries the ceiling.").optional(), "max_spend_cents": z.coerce.number().int().describe("Maximum spend in currency minor units (e.g., cents for USD).\n Exchange tracks cumulative spend against this cap.").optional(), "principal_domain": z.string().describe("Who granted this delegation (domain for public key lookup).").default(""), "principal_id": z.string().describe("Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").").default(""), "quota_period": z.string().describe("Quota reset period. How often the access/spend counters reset.\n Example: 30 days for monthly subscriptions — \"2592000s\" on the wire\n (proto-JSON encodes Duration as seconds; \"720h\" is not accepted).\n When absent, the quota is lifetime (bounded only by expires_at).").optional(), "revocation_uri": z.string().describe("Optional: URI for real-time revocation checking.\n Exchange MAY check this for high-value transactions.\n Not checked for routine low-value access (performance tradeoff).").optional(), "scopes": z.array(z.string()).describe("Scopes granted by this delegation. MUST be a subset of the\n principal's own scopes (attenuation — can only narrow, not widen).").optional(), "token": z.string().regex(new RegExp("^[A-Za-z0-9+/]*={0,2}$")).describe("Token bytes. A JWT (base64url-encoded JWS).").default(""), "token_format": z.string().describe("Token format: \"jwt\" (default). Empty is treated as \"jwt\". The field stays\n open for a future format.").default("") }).describe("Optional delegation — present when the requester acts on behalf of\n another entity (user, organization, upstream agent). On a query a Broker\n originated it is not honoured, because the Broker signs alone and nothing\n on that leg can pass its holder binding; the Exchange answers as if none\n were present (see Delegation).").optional(), "domain": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("REQUIRED. Bare host of the requester's key directory: the WBA directory at\n {domain}/.well-known/http-message-signatures-directory that publishes the\n agent's Ed25519 keys. It carries the bare-host shape \"Request recipient\"\n defines in the file header (a port allowed), for the same structural\n reason: a scheme, path or query smuggled in here would choose what gets\n fetched, not merely from where. It is never a free label. Every verifier\n reads it as the name of that directory, and the rule that binds it depends\n on whether the request is direct or relayed, which the receiver decides as\n \"Direct and relayed requests\" in the file header states:\n\n- Direct request. The agent's own RFC 9421 signature arrives because\n the agent sent the request itself. The verifier resolves the agent's\n keys from the COVERED `Signature-Agent` member, never from this field.\n It then MUST require this field to name that same directory: the host\n of the origin that member names, compared by\n the identity rule \"Request recipient\" defines (the shape check first,\n then case-folded, an absent port the same as \":443\", a subdomain a\n different party). A mismatch is refused as UNAUTHENTICATED with\n `request_auth_failure` SIGNATURE_INVALID: the signature verifies, but\n not for the requester the body names. A Broker applies this rule at\n BrokerService.ExecuteTransaction. At BrokerService.Resolve it refuses\n nothing for this field: every query it originates names the host of\n the origin that member names (see DiscoveryRequest.requester). An\n Exchange applies it on every request whose signer it does not admit as\n a Broker; a mismatch from a signer it does admit is a relayed request,\n one of the two cases below.\n - Purchase relayed through a Broker. BrokerService.ExecuteTransaction\n re-packages the purchase, so the request signature and the covered\n `Signature-Agent` member are the Broker's. They say only that the call comes\n from the Broker, and they sign no purchase. The Exchange MUST verify\n each item's AgentAcceptance, and the AgentRequestAcceptance, against\n the Ed25519 keys currently valid in the directory this field names\n (see AgentRequestAcceptance), never against the Broker's key. Those\n acceptances are the only agent signatures the Exchange sees, and the\n purchase belongs to the account of the directory they verify under\n (see \"Accounts\" in the file header).\n - Broker-led discovery. A ResourceQuery a Broker originated carries no agent\n signature, so nothing on that leg authenticates this field: it is the\n Broker's statement, under the Broker's own signature, of whom it\n queries for. A delegation on such a query is not honoured (see\n Delegation)."), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "id": z.string().min(1).max(255).describe("REQUIRED. A free label the agent chooses for attribution (e.g.\n \"agent-research-bot-001\"), 1 to 255 characters. Use it to tell apart the\n sub-agents or end customers that share one key directory. It is never\n identity: no verifier finds keys from it, and no party trusts it for an\n authentication or authorization decision. The requester's identity is\n `domain` together with the key that verifies. The acceptance signatures\n cover this label (see AgentAcceptancePayload), so a relaying Broker cannot\n rewrite it, but its value is still only what the agent says."), "name": z.string().describe("Human-readable name (e.g., \"Acme Research Assistant\").").optional(), "scopes": z.array(z.string()).max(64).describe("Entitlement scopes: the requester's own statement of what it is entitled\n to. On a query a Broker originated, they are the Broker's statement for the\n agent. They are signed and attributable, and the Exchange does not verify\n them. Scope coverage decides which terms the Exchange presents (see\n LicenseTerm.scopes, which also states when an Exchange MAY require a\n verified delegation); it is not access control. A false statement is\n traceable to the signing key, its directory and its account, and the\n agent's acceptance signs the scoped offer it buys; it is settled through\n reconciliation and the account relationship.\n\nEXISTENCE HIDING. A requester never learns about a resource outside its\n scopes. When the scopes leave no presentable offer for a resource, the\n Exchange answers with no offers and no absence reason (an OfferGroup with\n empty `offers` and `absence_reason` unset), exactly as for any resource\n with nothing to offer. A Broker relaying that answer adds no reason of its\n own. At purchase, an offer the Exchange presented is honoured until it\n expires, so a scope refusal cannot arise there. No reason in the protocol\n reports a scope shortfall: OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT and\n DENIAL_REASON_SCOPE_INSUFFICIENT are deprecated and never sent.\n\n Scope format: colon-separated segments, \"{domain}:{permission}\" or\n \"{profile}:{permission}\", optionally multi-segment (\"dist:US:CA\");\n matching is segment-wise per the rule below (no implicit hierarchy).\n Examples:\n \"credit:read\" — can access credit reports\n \"subscription:marketdata-2026\" — has active MarketData subscription\n \"academic:*\" — full access to academic resources\n \"internal:reports\" — can access internal reports\n \"*\" — unrestricted (public Exchange default)\n\n Matching is SEGMENT-WISE (\":\" separated). A granted scope G covers a\n required scope R iff, segment by segment, each G segment equals the\n corresponding R segment or is \"*\"; a terminal \"*\" matches all remaining\n segments. There is NO implicit prefix match, and a grant NARROWER than\n the requirement does not cover it (G must be equal-to-or-broader than R).\n Examples: \"dist:*\" covers \"dist:US\" and \"dist:US:CA\"; \"dist:US:*\" covers\n \"dist:US:CA\" but not \"dist:EU\"; bare \"dist\" covers only \"dist\"; granted\n \"dist:US:CA\" does NOT cover required \"dist:US\"; \"*\" covers everything.\n This same rule governs LicenseTerm.scopes — one algorithm protocol-wide.\n\n When empty, Exchange applies its default access policy (typically\n returns all publicly available resources).").optional(), "type": z.enum(["REQUESTER_TYPE_AGENT","REQUESTER_TYPE_HUMAN_TOOL","REQUESTER_TYPE_SERVICE","REQUESTER_TYPE_DELEGATED","REQUESTER_TYPE_RESEARCH"]).describe("What kind of entity is making this request.") }).describe("Requester identity — who is making this request, the scopes it states.\n The Broker forwards this to Exchanges in ResourceQuery.requester, with\n Requester.domain the host of the origin that the covered `Signature-Agent`\n member of the agent's verified signature names. A Requester.domain that\n names another host is never forwarded, and the Broker answers the request\n all the same (see \"Direct and relayed requests\" in the file header).\n REQUIRED: a discovery names its requester, whose Requester.id and\n Requester.domain are required in turn."), "search_filters": z.record(z.string(), z.any()).describe("Structured search filters (optional, alongside or instead of query).\n Keys are profile-specific: \"academic.topic\", \"news.category\",\n \"legal.jurisdiction\", etc. The Broker maps these to Exchange-specific\n query parameters.").optional(), "supported_profiles": z.array(z.string()).describe("Domain extension profiles the agent understands.\n\nThe Broker MUST forward this list unchanged in\n ResourceQuery.supported_profiles on every query it sends an Exchange, and\n forward an absent list as absent. It does not route by it: which Exchanges\n it queries is decided on other grounds, and each Exchange answers for its\n own profiles (see ResourceQuery.supported_profiles).\n\n Examples: [\"fora-academic-v1\"] — agent working on literature review").optional(), "uris": z.array(z.string()).max(256).describe("Resource URIs the agent wants. The Broker forwards these to Exchanges in\n ResourceQuery.uris. Optional when `query` / `search_filters` drive\n Broker-side discovery instead.").optional(), "ver": z.string().describe("FORA protocol version — \"1.0\". Stamped by the sender from a single\n constant; advisory on receive. See \"Protocol version\" in the file header.").default("") }).describe("DiscoveryRequest — Agent sends to Broker (Step 1).")))(); +export const DiscoveryRequestSchema = /* @__PURE__ */ (() => wire(z.object({ "acceptable_restrictions": z.array(z.object({ "axis": z.union([z.string().regex(new RegExp("^RESTRICTION_KIND_UNSPECIFIED$")), z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("Which axis (same enum as Restriction.kind): FUNCTION / GEOGRAPHY /\n USER_TYPE / OTHER.").default(0), "values": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("The values the query operates within on this axis — same token vocabulary\n as the terms (e.g. FUNCTION [\"ai-train\"], GEOGRAPHY [\"US\", \"EU\"]).").optional() }).describe("AcceptableRestriction — the limits a query operates within on one restriction\n axis, expressed in the same RestrictionKind vocabulary that terms use. The\n Exchange/Broker MAY pre-select offers whose term restrictions fall within\n these as a convenience (see Restriction); it is NOT enforcement — the agent\n self-selects and bears compliance.")).describe("The limits the agent will operate within, per restriction axis — see\n AcceptableRestriction. The Broker forwards these to Exchanges in\n ResourceQuery.acceptable_restrictions. Advisory selection inputs, not\n enforcement.").optional(), "constraints": z.object({ "budget_period": z.string().describe("Budget period (e.g. \"2592000s\" = 30 days; proto-JSON encodes Duration\n as seconds).").optional(), "budget_scope": z.string().describe("Budget scope identifier: whose budget this is.\n E.g. \"user:u-12345\" for per-user budgets, \"team:eng\" for per-team.").optional(), "delivery_preference": z.array(z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"])).describe("Preferred delivery methods, in order of preference.").optional(), "exchanges": z.array(z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260)).describe("Authorized Exchange domains, in the shape \"Request recipient\" defines in the\n file header. Broker queries only these. This is a FILTER over third parties,\n not an address — the recipient of the request carrying it is a separate\n question.").optional(), "max_data_age": z.string().describe("Maximum acceptable age of resource data. The Broker SHOULD\n exclude offers where (now - Offer.data_as_of) exceeds this duration.\n\nOnly relevant for DYNAMIC resources. Ignored for STATIC (content is\n immutable) and LIVE (content doesn't exist yet).\n\n Examples:\n 7 days — \"credit report updated within the last week\"\n 1 hour — \"stock snapshot from the last hour\"\n 30 days — \"drug interaction database updated this month\"").optional(), "max_hops": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Maximum number of hops the agent will allow its discovery to travel. It\n rides only on DiscoveryRequest, which terminates at the Broker, and a\n Broker originates its own queries rather than forwarding the agent's\n request, so no party receives or enforces it in this version: an Exchange\n never sees it, and an Exchange's own cap is\n WellKnownManifest.max_intermediary_hops. The field is kept for multi-hop\n Broker discovery, an open protocol question. Absent = no cap stated.").optional(), "max_price": z.object({ "amount": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Exact decimal string (not a float), e.g. \"19.99\". Denominated in `currency`.").default(""), "currency": z.string().default(""), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).optional() }).describe("Maximum price the agent is willing to pay.").optional(), "max_unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Maximum effective cost per unit, as an exact decimal string (not a float).").optional(), "period_budget": z.object({ "amount": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Exact decimal string (not a float), e.g. \"19.99\". Denominated in `currency`.").default(""), "currency": z.string().default(""), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).optional() }).describe("Per-period budget limit for the budget_scope.").optional(), "preferred_exchanges": z.array(z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260)).describe("Exchanges the agent has existing relationships with (subscriptions,\n contracts). The Broker SHOULD prefer these when resource is\n available — subscription resource has zero marginal cost.").optional(), "reporting_capable": z.boolean().describe("Whether the agent supports post-usage reporting.").optional() }).describe("Constraints for exchange filtering and offer selection.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "query": z.string().describe("Search query for Broker-side resource discovery.\n Used when the agent doesn't know specific URIs but wants the Broker\n to find matching resources across Exchanges.\n When present, the Broker interprets the query and discovers resources\n across Exchanges on the agent's behalf. Results returned as Offers\n in DiscoveryResponse, same as for specific URI requests.\n Can be used alongside uris (specific URIs + search in one request).").optional(), "requester": z.object({ "delegation": z.object({ "expires_at": z.string().datetime({ offset: true }).describe("Mirror of the chain's earliest `exp`. The Exchange MUST reject an expired\n token; the signed `exp` values decide, not this field.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "issuer": z.string().describe("Mirror of the ROOT token's `iss`, the identity configured with the trusted\n key (never an intermediate or terminal issuer). Informational: the\n Exchange never discovers a key from it, through OIDC, JWKS or otherwise.").optional(), "max_accesses": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Maximum number of accesses allowed under this delegation.\n Exchange tracks cumulative access count against this cap.\n Deny with DENIAL_REASON_QUOTA_EXCEEDED when count >= limit.\n For subscriptions with \"10,000 accesses/month\", this carries the ceiling.").optional(), "max_spend_cents": z.coerce.number().int().describe("Maximum spend in currency minor units (e.g., cents for USD).\n Exchange tracks cumulative spend against this cap.").optional(), "principal_domain": z.string().describe("Mirror of the ROOT token's `iss`, the configured trust anchor's identity.\n Informational: no key is looked up from it, and it establishes no trust.").default(""), "principal_id": z.string().describe("Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").\n Informational; not read by the verifier.").default(""), "quota_period": z.string().describe("Quota reset period. How often the access/spend counters reset.\n Example: 30 days for monthly subscriptions — \"2592000s\" on the wire\n (proto-JSON encodes Duration as seconds; \"720h\" is not accepted).\n When absent, the quota is lifetime (bounded only by expires_at).").optional(), "revocation_uri": z.string().describe("Informational. Revocation lookup is not part of the delegation profile:\n the verifier never fetches this URI. A chain is invalidated early by\n withdrawing the configured root key.").optional(), "scopes": z.array(z.string()).describe("Mirror of the terminal token's effective scopes. In the chain, each\n child's scopes are covered by its parent's under the scope-matching rule\n on Requester.scopes (attenuation — can only narrow, not widen).").optional(), "token": z.string().regex(new RegExp("^[A-Za-z0-9+/]{0,43691}={0,2}$")).max(43692).describe("Token bytes. A JWT (base64url-encoded JWS). At most 32768 bytes: the\n complete chain, every embedded parent included, must fit. The limit is a\n wire rule, checked before the token is parsed and independent of whether\n the bytes are a valid JWT; a longer token is an invalid request, not a\n per-item denial.").default(""), "token_format": z.string().describe("Token format: \"jwt\" (default). Empty is treated as \"jwt\". The field stays\n open for a future format.").default("") }).describe("Optional delegation — present when the requester acts on behalf of\n another entity (user, organization, upstream agent). On a query a Broker\n originated it is not honoured, because the Broker signs alone and nothing\n on that leg can pass its holder binding; the Exchange answers as if none\n were present (see Delegation).").optional(), "domain": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("REQUIRED. Bare host of the requester's key directory: the WBA directory at\n {domain}/.well-known/http-message-signatures-directory that publishes the\n agent's Ed25519 keys. It carries the bare-host shape \"Request recipient\"\n defines in the file header (a port allowed), for the same structural\n reason: a scheme, path or query smuggled in here would choose what gets\n fetched, not merely from where. It is never a free label. Every verifier\n reads it as the name of that directory, and the rule that binds it depends\n on whether the request is direct or relayed, which the receiver decides as\n \"Direct and relayed requests\" in the file header states:\n\n- Direct request. The agent's own RFC 9421 signature arrives because\n the agent sent the request itself. The verifier resolves the agent's\n keys from the COVERED `Signature-Agent` member, never from this field.\n It then MUST require this field to name that same directory: the host\n of the origin that member names, compared by\n the identity rule \"Request recipient\" defines (the shape check first,\n then case-folded, an absent port the same as \":443\", a subdomain a\n different party). A mismatch is refused as UNAUTHENTICATED with\n `request_auth_failure` SIGNATURE_INVALID: the signature verifies, but\n not for the requester the body names. A Broker applies this rule at\n BrokerService.ExecuteTransaction. At BrokerService.Resolve it refuses\n nothing for this field: every query it originates names the host of\n the origin that member names (see DiscoveryRequest.requester). An\n Exchange applies it on every request whose signer it does not admit as\n a Broker; a mismatch from a signer it does admit is a relayed request,\n one of the two cases below.\n - Purchase relayed through a Broker. BrokerService.ExecuteTransaction\n re-packages the purchase, so the request signature and the covered\n `Signature-Agent` member are the Broker's. They say only that the call comes\n from the Broker, and they sign no purchase. The Exchange MUST verify\n each item's AgentAcceptance, and the AgentRequestAcceptance, against\n the Ed25519 keys currently valid in the directory this field names\n (see AgentRequestAcceptance), never against the Broker's key. Those\n acceptances are the only agent signatures the Exchange sees, and the\n purchase belongs to the account of the directory they verify under\n (see \"Accounts\" in the file header).\n - Broker-led discovery. A ResourceQuery a Broker originated carries no agent\n signature, so nothing on that leg authenticates this field: it is the\n Broker's statement, under the Broker's own signature, of whom it\n queries for. A delegation on such a query is not honoured (see\n Delegation)."), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "id": z.string().min(1).max(255).describe("REQUIRED. A free label the agent chooses for attribution (e.g.\n \"agent-research-bot-001\"), 1 to 255 characters. Use it to tell apart the\n sub-agents or end customers that share one key directory. It is never\n identity: no verifier finds keys from it, and no party trusts it for an\n authentication or authorization decision. The requester's identity is\n `domain` together with the key that verifies. The acceptance signatures\n cover this label (see AgentAcceptancePayload), so a relaying Broker cannot\n rewrite it, but its value is still only what the agent says."), "name": z.string().describe("Human-readable name (e.g., \"Acme Research Assistant\").").optional(), "scopes": z.array(z.string()).max(64).describe("Entitlement scopes: the requester's own statement of what it is entitled\n to. On a query a Broker originated, they are the Broker's statement for the\n agent. They are signed and attributable, and the Exchange does not verify\n them. Scope coverage decides which terms the Exchange presents (see\n LicenseTerm.scopes, which also states when an Exchange MAY require a\n verified delegation); it is not access control. A false statement is\n traceable to the signing key, its directory and its account, and the\n agent's acceptance signs the scoped offer it buys; it is settled through\n reconciliation and the account relationship.\n\nEXISTENCE HIDING. A requester never learns about a resource outside its\n scopes. When the scopes leave no presentable offer for a resource, the\n Exchange answers with no offers and no absence reason (an OfferGroup with\n empty `offers` and `absence_reason` unset), exactly as for any resource\n with nothing to offer. A Broker relaying that answer adds no reason of its\n own. At purchase, an offer the Exchange presented is honoured until it\n expires, so a scope refusal cannot arise there. No reason in the protocol\n reports a scope shortfall: OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT and\n DENIAL_REASON_SCOPE_INSUFFICIENT are deprecated and never sent.\n\n Scope format: colon-separated segments, \"{domain}:{permission}\" or\n \"{profile}:{permission}\", optionally multi-segment (\"dist:US:CA\");\n matching is segment-wise per the rule below (no implicit hierarchy).\n Examples:\n \"credit:read\" — can access credit reports\n \"subscription:marketdata-2026\" — has active MarketData subscription\n \"academic:*\" — full access to academic resources\n \"internal:reports\" — can access internal reports\n \"*\" — unrestricted (public Exchange default)\n\n Matching is SEGMENT-WISE (\":\" separated). A granted scope G covers a\n required scope R iff, segment by segment, each G segment equals the\n corresponding R segment or is \"*\"; a terminal \"*\" matches all remaining\n segments, of which there must be at least one. There is NO implicit prefix\n match, and a grant NARROWER than the requirement does not cover it (G must\n be equal-to-or-broader than R), so a grant with more segments than the\n requirement never covers it, even when its last segment is \"*\". A \"*\" in R\n is a literal segment: a narrow grant never covers a wildcard requirement.\n Matching is case-sensitive and never trims. Examples: \"dist:*\" covers\n \"dist:US\", \"dist:US:CA\" and \"dist:US:*\" but not \"dist\"; \"dist:US:*\" covers\n \"dist:US:CA\" but not \"dist:EU\" or \"dist:US\"; \"publisher:*:read\" covers\n \"publisher:news:read\" but not \"publisher:news:*\"; bare \"dist\" covers only\n \"dist\"; granted \"dist:US:CA\" does NOT cover required \"dist:US\"; \"*\" covers\n everything. This same rule governs LicenseTerm.scopes and the attenuation\n of a delegation child against its parent — one algorithm protocol-wide.\n\n When empty, Exchange applies its default access policy (typically\n returns all publicly available resources).").optional(), "type": z.enum(["REQUESTER_TYPE_AGENT","REQUESTER_TYPE_HUMAN_TOOL","REQUESTER_TYPE_SERVICE","REQUESTER_TYPE_DELEGATED","REQUESTER_TYPE_RESEARCH"]).describe("What kind of entity is making this request.") }).describe("Requester identity — who is making this request, the scopes it states.\n The Broker forwards this to Exchanges in ResourceQuery.requester, with\n Requester.domain the host of the origin that the covered `Signature-Agent`\n member of the agent's verified signature names. A Requester.domain that\n names another host is never forwarded, and the Broker answers the request\n all the same (see \"Direct and relayed requests\" in the file header).\n REQUIRED: a discovery names its requester, whose Requester.id and\n Requester.domain are required in turn."), "search_filters": z.record(z.string(), z.any()).describe("Structured search filters (optional, alongside or instead of query).\n Keys are profile-specific: \"academic.topic\", \"news.category\",\n \"legal.jurisdiction\", etc. The Broker maps these to Exchange-specific\n query parameters.").optional(), "supported_profiles": z.array(z.string()).describe("Domain extension profiles the agent understands.\n\nThe Broker MUST forward this list unchanged in\n ResourceQuery.supported_profiles on every query it sends an Exchange, and\n forward an absent list as absent. It does not route by it: which Exchanges\n it queries is decided on other grounds, and each Exchange answers for its\n own profiles (see ResourceQuery.supported_profiles).\n\n Examples: [\"fora-academic-v1\"] — agent working on literature review").optional(), "uris": z.array(z.string()).max(256).describe("Resource URIs the agent wants. The Broker forwards these to Exchanges in\n ResourceQuery.uris. Optional when `query` / `search_filters` drive\n Broker-side discovery instead.").optional(), "ver": z.string().describe("FORA protocol version — \"1.0\". Stamped by the sender from a single\n constant; advisory on receive. See \"Protocol version\" in the file header.").default("") }).describe("DiscoveryRequest — Agent sends to Broker (Step 1).")))(); -export const DiscoveryResponseSchema = /* @__PURE__ */ (() => wire(z.object({ "absence_reason": z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"]).describe("Existence-oracle note: an authorization-flavored reason (NOT_AUTHORIZED,\n NOT_IN_CATALOG, CONTENT_BLOCKED) confirms a resource exists\n and why access was refused. Resolve surfaces the same oracle at the broker\n that OfferGroup.absence_reason does at the Exchange, so the same mitigation\n applies: where existence itself must stay hidden, the Broker MAY omit the\n reason (leave this unset) rather than reveal it. See the threat model.\n A scope shortfall never reaches this field: the Exchange answers it with\n no absence reason, and the Broker MUST NOT supply one (the existence-hiding\n rule, stated on Requester.scopes).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "offer_groups": z.array(z.object({ "absence_reason": z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"]).describe("Why no offers are available for this URI.\n Set only when `offers` is empty, and not always then: a resource the\n requester's scopes leave with no presentable offer is answered with empty\n `offers` and this field unset (the existence-hiding rule, stated on\n Requester.scopes). Enables agents/Brokers to distinguish\n \"resource not in catalog\" from \"resource blocked for your use case\" without\n trial-and-error transactions. Analogous to OpenRTB nbr codes and\n Shutterstock per-item error metadata in batch responses.").optional(), "discovery_method": z.enum(["DISCOVERY_METHOD_EXCHANGE","DISCOVERY_METHOD_SEARCH","DISCOVERY_METHOD_RECOMMENDATION","DISCOVERY_METHOD_SYNDICATION"]).describe("How this URI was discovered by the Broker (v2 extension point).\n v1: always DISCOVERY_METHOD_EXCHANGE (Broker queried an Exchange).\n v2: may include DISCOVERY_METHOD_SEARCH (URI found via search engine like Exa),\n DISCOVERY_METHOD_RECOMMENDATION, etc. The Broker discovers URIs\n through any source, then routes through Exchange for pricing/transaction.\n The discovery method does not affect the transaction flow — it's metadata\n for the agent to understand how the resource was found.").optional(), "offers": z.array(z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"fora.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("ResourceAttestation — Signed envelope of claims from a trusted party.\n\nA provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"fora.attestation.claims_schema\"].")).describe("Signed attestations about the resource at this URI.\n Attestations provide cryptographic proof of\n resource properties from trusted parties (providers or verification vendors).\n\nThree verification levels determine what is independently verifiable:\n Level 0 (no attestations): Resource may carry identifiers (DOI, IPTC GUID)\n for identification, but nothing is cryptographically verifiable.\n Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): Provider signs own claims with Ed25519 key.\n Agent can independently verify content hash and token count.\n CDN delivery failure + content hash mismatch are auto-disputable.\n Level 2 (third-party attested): Independent verification vendor crawled\n the resource and attested to its properties. Agent trusts the attestation\n (does not re-verify hash). Token count discrepancy is auto-disputable\n when corroborated by CDN response size.\n\n Multiple attestations may be present (e.g., provider self-attestation\n plus a third-party verification). Agents choose which to trust.").optional(), "data_as_of": z.string().datetime({ offset: true }).describe("When the offered data was current. For dynamic resources\n (resource_mutability = DYNAMIC), this is the snapshot timestamp.\n Enables the Broker to evaluate freshness: \"this credit report\n reflects data as of March 18\" or \"this drug database was updated today.\"\n\nNot set for STATIC resources (content doesn't change) or LIVE\n resources (content doesn't exist yet).\n\n The Broker compares this against RequestConstraints.max_data_age\n to filter stale offers. Example: agent requests max_data_age = 7 days,\n Broker drops offers where now() - data_as_of > 7 days.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource will be delivered.").default(0), "exchange": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("REQUIRED. Bare host of the Exchange that issued this offer (e.g.\n \"exchange.example\" or \"exchange.example:8081\"), in the form \"Request\n recipient\" defines in the file header. This is the execute-routing target:\n the agent, or a relaying Broker, sends the ExecuteTransaction call for this\n offer to this Exchange, and a Broker relaying a mixed batch groups the items\n by this value. Because it is an ordinary Offer field it falls inside the\n signed bytes (see `signature` below — the signature covers every field\n except `signature` / `signature_algorithm`), so an intermediary cannot\n redirect the execute call to a different Exchange without invalidating the\n offer, and it is what retires the X-FORA-Exchange-Endpoint transport header.\n It is also the audience statement of an ExecuteTransaction, which is why\n TransactionRequest carries no top-level `exchange`: on receipt, an Exchange\n MUST reject the request unless EVERY item's offer.exchange names its own\n domain. Presence is enforced because an empty value is unroutable — a\n relaying Broker has nothing to group or dial on, and the swap-protection\n above is vacuous when the signed bytes carry no recipient at all."), "expires_at": z.string().datetime({ offset: true }).describe("When this offer expires (ISO 8601).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "iab_categories": z.array(z.string()).describe("IAB Content Taxonomy category codes.\n Enables agents to filter offers by topic (e.g., \"only finance resources\").\n Uses IAB Content Taxonomy 3.1 codes.").optional(), "identity": z.object({ "c2pa_manifest": z.string().describe("C2PA content credentials manifest URI.\n Points to a sidecar or embedded C2PA manifest for this resource.\n C2PA-aware agents MAY follow this URI to validate the full provenance\n chain (creator identity, transformation history, ingredient composition)\n using C2PA libraries (JUMBF/COSE Sign1). C2PA-unaware agents can rely\n on c2pa_status and c2pa-bridged attestation claims instead.\n\nFormats:\n Sidecar: HTTPS URI to a .c2pa manifest file\n Embedded: same URI as canonical_url (manifest is inside the asset)\n Content Credentials Cloud: https://contentcredentials.org/verify?uri=...").optional(), "c2pa_status": z.enum(["C2PA_STATUS_TRUSTED","C2PA_STATUS_VALID","C2PA_STATUS_INVALID","C2PA_STATUS_ABSENT"]).describe("The full C2PA validation details (signer identity, trust list,\n action history, training/mining status) are carried in a\n ResourceAttestation with c2pa.* claims — see fora-c2pa-v1 profile.").optional(), "canonical_url": z.string().describe("Provider's authoritative URL for this resource (rel=\"canonical\").\n Always available. Different per provider for syndicated content.").optional(), "content_hash": z.string().describe("Hash of the content. Interpretation depends on hash_method:\n \"simhash-v1\" → locality-sensitive hash, for fuzzy dedup (Level 1)\n \"sha256\" → exact-match integrity hash (Level 2)\n\nLevel 1 (SimHash): computed by Exchange from extracted text.\n Agent verifies that fetched content is \"substantially similar.\"\n Tolerates dynamic page elements.\n\n Level 2 (SHA-256): computed by provider from deterministic payload.\n Agent verifies exact match. Requires provider to serve consistent\n content (e.g., API endpoint, static HTML, structured JSON).\n Mismatch = dispute. Commands premium pricing.").optional(), "doi": z.string().describe("Digital Object Identifier — persistent, never changes.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().describe("Hash algorithm and verification level.\n Examples: \"simhash-v1\", \"minhash-v1\", \"sha256\", \"sha384\"").optional(), "iptc_guid": z.string().describe("IPTC NewsML-G2 globally unique identifier.\n Present when resource flows through news wire syndication (AP, Reuters).").optional(), "isni": z.string().describe("International Standard Name Identifier for the creator.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Drives hash verification behavior:\n STATIC: content_hash is stable. Agent SHOULD verify delivered content matches.\n DYNAMIC: content changes between offer and fetch (credit reports, drug databases).\n content_hash reflects state at offer generation time. Hash mismatch is\n expected and MUST NOT trigger automatic dispute.\n LIVE: content does not exist at offer time (streaming feeds, live broadcasts).\n content_hash is not applicable. The \"resource\" is the stream endpoint.\n\n Validated across 18 use cases: static content (articles, patents, legislation),\n dynamic data (credit reports, drug interactions, stock snapshots), and live\n streams (MarketData quotes, NPR broadcast, news monitoring feeds)."), "soft_binding": z.string().describe("Soft binding hash — content-derived identifier that survives format\n transcoding (resolution changes, compression, PDF-to-text extraction).\n Extracted from C2PA soft binding assertion when present.\n Enables post-delivery verification when the hard binding hash breaks\n due to legitimate format conversion.\n\nAlgorithm specified in soft_binding_method. Values are algorithm-specific\n (e.g., perceptual hash hex string, watermark identifier).").optional(), "soft_binding_method": z.string().describe("Algorithm used for soft_binding.\n Examples: \"phash-v1\" (perceptual hash), \"c2pa-watermark\" (C2PA invisible\n watermark), \"chromaprint\" (audio fingerprint).").optional() }).describe("Resource identity for cross-exchange deduplication.\n Enables Brokers to recognize the same resource offered by\n different Exchanges and compare pricing.").optional(), "offer_id": z.string().describe("Unique identifier for this offer, assigned by the Exchange.\n Opaque to the caller: not derived from the resource, its URL, or any\n other field, and carries no meaning beyond identifying this offer.\n Two offers for the same resource have different offer_ids.").default(""), "previews": z.array(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("Preview — Lightweight resource preview for offer evaluation.\n\nThe Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no FORA transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")).describe("Lightweight previews for offer evaluation.\n The Exchange holds URLs (50–200 bytes each); the provider's CDN serves\n the actual bytes. Agents fetch previews only when evaluating offers —\n not on every discovery query. Multiple previews at different sizes\n allow agents to pick the cheapest fetch for their evaluation needs.\n\nPer content type:\n Image: watermarked thumbnail (150–450px JPEG)\n Video: short clip (10–30s MP4, watermarked)\n Audio: short clip (15–30s MP3, low-bitrate or watermarked)\n Text: snippet or abstract (first 200 words as text/plain)\n Data: sample records (1–3 rows as application/json)\n Stream: optional frame capture or none (streams are priced by time)\n\n Modeled after Shutterstock (multi-size thumbnail URLs),\n Spotify (preview_url to 30s clip), IIIF (parameterized image URLs),\n and OpenRTB native (img.url + dimensions).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("The offer's price, and its ONLY price. An offer represents a single\n licensing arrangement: each projected LicenseTerm yields its own offer, and\n the Exchange moves that term's price here, so the term inside `terms`\n carries no pricing of its own (the offer.terms.pricing_unset rule). This is\n the price execute charges, the price a Broker ranks and compares across\n Exchanges, and the price a metered purchase is charged at. A resource with\n multiple alternative terms (e.g. dual-licensed) produces multiple separate\n offers, one per term — never one offer with a \"headline\" picked among them.\n\nIt derives from exactly one catalog term: the term's pricing as the\n publisher declared it (ResourceEntry.terms). Each offer derives from one\n term, and the offer carries no second copy, so the offer's price and its\n term's price cannot disagree.\n\n On a metered (PER_UNIT) offer, the rate and the estimate a metered\n purchase is charged at are read from here (see Pricing). The estimate is\n optional. When the publisher states one — on the term's own pricing, or\n once for the resource on ResourceEntry.estimated_quantity — the Exchange\n carries it here as estimated_quantity, the term's own estimate taking\n precedence. When the publisher states none, the offer may carry none, and\n the purchase then charges one unit.").optional(), "reporting": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds). A report that arrives after\n the window closed is refused with USAGE_REPORT_REJECTION_REASON_WINDOW_EXPIRED\n (Connect code failed_precondition).").optional() }).describe("Post-usage reporting requirements for this offer.").optional(), "signature": z.string().describe("REQUIRED. Hex-encoded detached Ed25519 signature over the canonical\n serialization of the ENTIRE Offer — every field, including `pricing`,\n `terms` (the full licensing payload), `expires_at`, and `exchange`. Only\n `signature` and `signature_algorithm` are excluded from the signed bytes.\n `expires_at` is signed so the offer's validity window is\n integrity-protected: a relaying Broker cannot extend (or shorten) the TTL\n of a signed offer to replay it outside the window the Exchange intended.\n\nCANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n UNKNOWN FIELDS. A canonicalizer either OMITS content it has no schema for or\n PRESERVES it, and the rule follows from which:\n\n - OMITTING (e.g. proto-JSON, which emits only schema-defined fields): such a\n canonicalizer CANNOT reproduce the signed bytes of a message carrying\n unknown fields — what it renders silently drops part of what the signer\n covered. It MUST refuse the message rather than emit the reduced bytes,\n and a verifier built on it MUST reject rather than verify over them. The\n refusal binds at EVERY depth: a nested message and each element of a\n repeated or map field carries its own unknown-field set.\n - PRESERVING (a canonicalizer that carries unrecognized members through):\n it reproduces the signed bytes faithfully, so there is nothing to refuse.\n\n Either way an APPENDED field cannot pass: an omitting canonicalizer refuses\n the message, and a preserving one renders the appended member into bytes the\n signer never covered, so the signature fails. Without the refusal the omitting\n case would fail OPEN — an intermediary could add unknown fields to an\n already-signed message and leave its signature verifying, smuggling\n unauthenticated content through a message the recipient treats as verified.\n\n Extensions therefore ride in `ext` / `ext_critical`, which are defined fields\n and inside the signed bytes — never as undeclared field numbers.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.").default(""), "signature_algorithm": z.string().describe("JOSE/JWA algorithm identifier (RFC 8037 §3.1). Always 'EdDSA' for\n Ed25519. Advisory only: this field is cleared before the canonical\n payload is signed, so it is not covered by the signature.").default(""), "subscription_id": z.string().describe("If set, this offer is available under an existing subscription/deal.\n No per-request billing — usage tracked against subscription quota.\n Pricing.rate = \"0\" for subscription offers (zero marginal cost).\n The Broker SHOULD prefer subscription offers when available.").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("SubscriptionQuotaInfo — Proactive quota signaling for subscription access.\n\nAnalogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Subscription quota state, when this offer is under a subscription.\n Enables the agent to see remaining quota before committing.\n Multiple entries when the subscription has independent quotas\n (e.g., access count + spend cap).").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Obligation — A post-use behavioral requirement attached to a LicenseTerm.\n\nExamples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).max(64).describe("Post-use behavioral requirements.\n At most 64, for the reason quotas carries.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. Where it is required depends on the message that\n holds the term, so the requirement is a rule of that message rather than of\n this field:\n - On a catalog term (ResourceEntry.terms) it is REQUIRED regardless of\n semantics — an agent cannot act on a priceless term, so absent Pricing\n is a validation error at ingest (resource_entry.terms.pricing_required).\n model = FREE must be stated explicitly (absent Pricing is not free). A\n REFERENCE_ONLY term states its price here too; its License governs the\n human-readable terms but does not replace the machine-readable price.\n - On the term an offer carries (Offer.terms) it MUST be unset\n (offer.terms.pricing_unset): the offer's price is Offer.pricing, stated\n once.").optional(), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The unit being capped — an open vocabulary axis.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quota — A usage cap the term declares.\n\nA quota states how much a licensee may consume under the term before the\n term expires or must be renegotiated. Like a restriction, it is a declared\n term the parties agree to. Quotas are NOT billing quantities — billing is in\n Pricing.\n\n The metric vocabulary is authored ONLY in the (fora.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).max(64).describe("Usage caps. The agent must not exceed any individual Quota.\n At most 64, the bound every per-message list in this contract carries when\n no rule walks it more than once. It bounds what one term may carry, not the\n work of checking one — a validator walks every element it is handed before\n the cap is reported, so the cost of checking is bounded at the transport.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to. Defined-only: the axis set is\n CLOSED, and a number outside it is refused rather than ignored. A custom\n axis is RESTRICTION_KIND_OTHER, whose meaning rides in permitted/prohibited,\n so a new number was never the extension mechanism — accepting one would\n admit a restriction no consumer can evaluate onto a term whose default is\n BINDING (see advisory below), which fails open on the axis a publisher most\n needs honoured. Closing the axis does NOT bound the cost of the one-per-kind\n rule below, and must not be read as doing so: a number this rule refuses is\n still distinct from every other, so that rule's all() finds no duplicate to\n stop on and walks the list in full anyway. Its cost is bounded by the size\n test the rule itself carries."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restriction — A single constraint on one licensing dimension.\n\nRestrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: they are declared terms the parties agree to when the\n agent accepts the offer. The AGENT self-selects the term whose restrictions it\n can honour and bears compliance. The Exchange never enforces a restriction:\n it is NOT an Exchange-side gate the requester must pass to see or buy a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and by the scopes in the requester's signed\n request — see LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (fora.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — FORA user/organization categories")).max(8).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.\n At most 8, and this list is the one of the three that does NOT carry the\n contract's usual 64: only one restriction per axis is valid, the axis enum\n is defined-only, so four is the longest conformant list and eight leaves\n room for an axis this version does not have. Like the caps on quotas and\n obligations, this one bounds the DOCUMENT — how many restrictions one term\n may carry — and not the work of checking it: a validator walks every element\n it is handed before any cardinality rule is reported, so an over-cap list is\n traversed in full on its way to being refused.\n\nWhat makes this list different is that one rule walks it against ITSELF. The\n one-per-kind rule below is quadratic, so it carries its own size test and\n stays silent above this cap; a conformance guard holds the two numbers equal,\n because a cap raised without the test would leave the lists in between\n unchecked for duplicate axes and accepted. The neighbouring disjointness rule\n on each element is quadratic only in that element's two token lists, both\n capped at 64, so its cost is bounded per restriction and linear across the\n list — it needs no such test.").optional(), "scopes": z.array(z.string()).max(64).describe("Scope-gating: the Exchange presents an offer for this term only when the\n scopes in the requester's signed request (Requester.scopes) cover ALL of\n these scopes (AND-semantics). Empty = public. A subscription term is\n Pricing{model:FREE} + scopes:[\"subscription:...\"].\n\nRequester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n also require a verified delegation that grants these scopes before it\n presents the term. To a requester that presents none, it answers as if the\n term did not exist, as existence hiding requires (see Requester.scopes). An\n offer the Exchange presented is honoured until it expires.\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("LicenseTerm — Universal licensing unit.\n\nOne LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation — with one move: on an offer the\n term's price is Offer.pricing, and the term itself carries none.\n\n Validation rules:\n - Pricing MUST be present on EVERY catalog term (ResourceEntry.terms),\n regardless of semantics — the resource_entry.terms.pricing_required\n rule. Absent Pricing → reject at ingest: an agent cannot act on a term\n with no price. This holds for REFERENCE_ONLY too — its License governs\n the human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - Pricing MUST be unset on the term an offer carries (Offer.terms) — the\n offer.terms.pricing_unset rule. The offer's price is Offer.pricing, and\n each offer derives from exactly one catalog term, so the price is stated\n once and cannot disagree with itself. The requirement is therefore a\n rule of the message that holds the term, not of LicenseTerm.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - A PER_UNIT term MAY state Pricing.estimated_quantity. It is not\n required, on the pushed term or on the offer it projects to — see\n Pricing.estimated_quantity.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).min(1).max(1).describe("The licensing term this offer sells, sourced from the publisher's\n ResourceEntry. EXACTLY ONE, and the bound is enforced rather than asked\n for: an offer IS a single licensing arrangement, so N terms on a resource\n project to N offers on that resource, each selling one of them — see\n `pricing` above. An offer with no term would be an offer projected from\n nothing, which is why the floor is one and not zero. The field is\n `repeated` because it shipped that way in v1 and renumbering a released\n field is not available to us, so the cardinality rides as a validation\n rule instead. `ResourceEntry.terms` is the plural side, bounded at 32: a\n resource carries many terms, an offer sells one of them.\n\nThe distinction is not cosmetic. `pricing` is what a Broker ranks on and\n what execute charges, so a term that is not on its own offer has no price\n a Broker can compare and no offer_id an agent can buy. Fusing several\n terms onto one offer makes every term but the first unsellable — a\n dual-licensed resource sells only under whichever arrangement the\n publisher happened to store first.\n\n The term here carries everything the publisher declared EXCEPT its price:\n its `pricing` MUST be unset (the offer.terms.pricing_unset rule), because\n the offer's price is `pricing` above. Its semantics, license,\n restrictions, quotas, obligations, scopes and part_label are the\n publisher's, unchanged.\n\n Where a term is reachable only under an existing subscription, its offer\n carries `subscription_id` and prices at zero marginal cost; the terms a\n requester may see at all are selected by `LicenseTerm.scopes`.\n\n See: Universal Licensing Core section.").optional(), "title": z.string().describe("Resource title (human-readable, for display/logging).").optional() }).describe("Offer — A single resource offer from an Exchange.\n\nCombines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via fora-comp-v1 extension profile.")).describe("Zero or more offers for this URI. Empty = resource not available.").optional(), "restriction_filters": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("When absence_reason = RESTRICTION_FILTERED, the restriction axes that drove\n the convenience pre-filter, in the same RestrictionKind vocabulary the terms\n use (e.g. [GEOGRAPHY] when the requester's stated geography matched no term).\n Advisory diagnostics, not an enforcement verdict.").optional(), "uri": z.string().describe("The URI this group of offers is for (echoed from ResourceQuery.uris).").default("") }).describe("OfferGroup — Offers for a single requested URI.\n Enables multi-URI batch queries where the caller needs to know\n which offers correspond to which requested resource.")).describe("Offers grouped by requested URI — the sole offer representation in this\n response. One OfferGroup per URI the agent asked for (echoed in\n OfferGroup.uri); a group with no offers carries OfferGroup.absence_reason\n explaining why. Each contained Offer is the full signed Offer the Exchange\n issued (including Offer.exchange, the execute-routing target), forwarded by\n the Broker unchanged so the agent can verify the signature end to end.").optional(), "ver": z.string().describe("FORA protocol version — \"1.0\". Stamped by the sender from a single\n constant; advisory on receive. See \"Protocol version\" in the file header.").default("") }).describe("DiscoveryResponse — Broker returns to Agent (Step 6).\n\nCarries discovery results only: the offers the Broker gathered across\n Exchanges, grouped by the URI they were requested for. Committing to an offer\n is a separate exchange on the execute path; that per-transaction result\n (transaction_id, billing_id, cost, delivery_method, retrieval endpoint, …)\n is returned by TransactionResponse, or by BrokerTransactionResponse when the\n purchase goes through BrokerService.ExecuteTransaction, not here.")))(); +export const DiscoveryResponseSchema = /* @__PURE__ */ (() => wire(z.object({ "absence_reason": z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"]).describe("Existence-oracle note: an authorization-flavored reason (NOT_AUTHORIZED,\n NOT_IN_CATALOG, CONTENT_BLOCKED) confirms a resource exists\n and why access was refused. Resolve surfaces the same oracle at the broker\n that OfferGroup.absence_reason does at the Exchange, so the same mitigation\n applies: where existence itself must stay hidden, the Broker MAY omit the\n reason (leave this unset) rather than reveal it. See the threat model.\n A scope shortfall never reaches this field: the Exchange answers it with\n no absence reason, and the Broker MUST NOT supply one (the existence-hiding\n rule, stated on Requester.scopes).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "offer_groups": z.array(z.object({ "absence_reason": z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"]).describe("Why no offers are available for this URI.\n Set only when `offers` is empty, and not always then: a resource the\n requester's scopes leave with no presentable offer is answered with empty\n `offers` and this field unset (the existence-hiding rule, stated on\n Requester.scopes). Enables agents/Brokers to distinguish\n \"resource not in catalog\" from \"resource blocked for your use case\" without\n trial-and-error transactions. Analogous to OpenRTB nbr codes and\n Shutterstock per-item error metadata in batch responses.").optional(), "discovery_method": z.enum(["DISCOVERY_METHOD_EXCHANGE","DISCOVERY_METHOD_SEARCH","DISCOVERY_METHOD_RECOMMENDATION","DISCOVERY_METHOD_SYNDICATION"]).describe("How this URI was discovered by the Broker (v2 extension point).\n v1: always DISCOVERY_METHOD_EXCHANGE (Broker queried an Exchange).\n v2: may include DISCOVERY_METHOD_SEARCH (URI found via search engine like Exa),\n DISCOVERY_METHOD_RECOMMENDATION, etc. The Broker discovers URIs\n through any source, then routes through Exchange for pricing/transaction.\n The discovery method does not affect the transaction flow — it's metadata\n for the agent to understand how the resource was found.").optional(), "offers": z.array(z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"fora.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("ResourceAttestation — Signed envelope of claims from a trusted party.\n\nA provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"fora.attestation.claims_schema\"].")).describe("Signed attestations about the resource at this URI.\n Attestations provide cryptographic proof of\n resource properties from trusted parties (providers or verification vendors).\n\nThree verification levels determine what is independently verifiable:\n Level 0 (no attestations): Resource may carry identifiers (DOI, IPTC GUID)\n for identification, but nothing is cryptographically verifiable.\n Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): Provider signs own claims with Ed25519 key.\n Agent can independently verify content hash and token count.\n CDN delivery failure + content hash mismatch are auto-disputable.\n Level 2 (third-party attested): Independent verification vendor crawled\n the resource and attested to its properties. Agent trusts the attestation\n (does not re-verify hash). Token count discrepancy is auto-disputable\n when corroborated by CDN response size.\n\n Multiple attestations may be present (e.g., provider self-attestation\n plus a third-party verification). Agents choose which to trust.").optional(), "data_as_of": z.string().datetime({ offset: true }).describe("When the offered data was current. For dynamic resources\n (resource_mutability = DYNAMIC), this is the snapshot timestamp.\n Enables the Broker to evaluate freshness: \"this credit report\n reflects data as of March 18\" or \"this drug database was updated today.\"\n\nNot set for STATIC resources (content doesn't change) or LIVE\n resources (content doesn't exist yet).\n\n The Broker compares this against RequestConstraints.max_data_age\n to filter stale offers. Example: agent requests max_data_age = 7 days,\n Broker drops offers where now() - data_as_of > 7 days.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource will be delivered.").default(0), "exchange": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("REQUIRED. Bare host of the Exchange that issued this offer (e.g.\n \"exchange.example\" or \"exchange.example:8081\"), in the form \"Request\n recipient\" defines in the file header. This is the execute-routing target:\n the agent, or a relaying Broker, sends the ExecuteTransaction call for this\n offer to this Exchange, and a Broker relaying a mixed batch groups the items\n by this value. Because it is an ordinary Offer field it falls inside the\n signed bytes (see `signature` below — the signature covers every field\n except `signature` / `signature_algorithm`), so an intermediary cannot\n redirect the execute call to a different Exchange without invalidating the\n offer, and it is what retires the X-FORA-Exchange-Endpoint transport header.\n It is also the audience statement of an ExecuteTransaction, which is why\n TransactionRequest carries no top-level `exchange`: on receipt, an Exchange\n MUST reject the request unless EVERY item's offer.exchange names its own\n domain. Presence is enforced because an empty value is unroutable — a\n relaying Broker has nothing to group or dial on, and the swap-protection\n above is vacuous when the signed bytes carry no recipient at all."), "expires_at": z.string().datetime({ offset: true }).describe("When this offer expires (ISO 8601). An offer for a term gated behind a\n verified delegation MUST NOT expire later than that delegation chain's\n effective expiry, the earliest `exp` in the chain (see Delegation).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "iab_categories": z.array(z.string()).describe("IAB Content Taxonomy category codes.\n Enables agents to filter offers by topic (e.g., \"only finance resources\").\n Uses IAB Content Taxonomy 3.1 codes.").optional(), "identity": z.object({ "c2pa_manifest": z.string().describe("C2PA content credentials manifest URI.\n Points to a sidecar or embedded C2PA manifest for this resource.\n C2PA-aware agents MAY follow this URI to validate the full provenance\n chain (creator identity, transformation history, ingredient composition)\n using C2PA libraries (JUMBF/COSE Sign1). C2PA-unaware agents can rely\n on c2pa_status and c2pa-bridged attestation claims instead.\n\nFormats:\n Sidecar: HTTPS URI to a .c2pa manifest file\n Embedded: same URI as canonical_url (manifest is inside the asset)\n Content Credentials Cloud: https://contentcredentials.org/verify?uri=...").optional(), "c2pa_status": z.enum(["C2PA_STATUS_TRUSTED","C2PA_STATUS_VALID","C2PA_STATUS_INVALID","C2PA_STATUS_ABSENT"]).describe("The full C2PA validation details (signer identity, trust list,\n action history, training/mining status) are carried in a\n ResourceAttestation with c2pa.* claims — see fora-c2pa-v1 profile.").optional(), "canonical_url": z.string().describe("Provider's authoritative URL for this resource (rel=\"canonical\").\n Always available. Different per provider for syndicated content.").optional(), "content_hash": z.string().describe("Hash of the content. Interpretation depends on hash_method:\n \"simhash-v1\" → locality-sensitive hash, for fuzzy dedup (Level 1)\n \"sha256\" → exact-match integrity hash (Level 2)\n\nLevel 1 (SimHash): computed by Exchange from extracted text.\n Agent verifies that fetched content is \"substantially similar.\"\n Tolerates dynamic page elements.\n\n Level 2 (SHA-256): computed by provider from deterministic payload.\n Agent verifies exact match. Requires provider to serve consistent\n content (e.g., API endpoint, static HTML, structured JSON).\n Mismatch = dispute. Commands premium pricing.").optional(), "doi": z.string().describe("Digital Object Identifier — persistent, never changes.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().describe("Hash algorithm and verification level.\n Examples: \"simhash-v1\", \"minhash-v1\", \"sha256\", \"sha384\"").optional(), "iptc_guid": z.string().describe("IPTC NewsML-G2 globally unique identifier.\n Present when resource flows through news wire syndication (AP, Reuters).").optional(), "isni": z.string().describe("International Standard Name Identifier for the creator.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Drives hash verification behavior:\n STATIC: content_hash is stable. Agent SHOULD verify delivered content matches.\n DYNAMIC: content changes between offer and fetch (credit reports, drug databases).\n content_hash reflects state at offer generation time. Hash mismatch is\n expected and MUST NOT trigger automatic dispute.\n LIVE: content does not exist at offer time (streaming feeds, live broadcasts).\n content_hash is not applicable. The \"resource\" is the stream endpoint.\n\n Validated across 18 use cases: static content (articles, patents, legislation),\n dynamic data (credit reports, drug interactions, stock snapshots), and live\n streams (MarketData quotes, NPR broadcast, news monitoring feeds)."), "soft_binding": z.string().describe("Soft binding hash — content-derived identifier that survives format\n transcoding (resolution changes, compression, PDF-to-text extraction).\n Extracted from C2PA soft binding assertion when present.\n Enables post-delivery verification when the hard binding hash breaks\n due to legitimate format conversion.\n\nAlgorithm specified in soft_binding_method. Values are algorithm-specific\n (e.g., perceptual hash hex string, watermark identifier).").optional(), "soft_binding_method": z.string().describe("Algorithm used for soft_binding.\n Examples: \"phash-v1\" (perceptual hash), \"c2pa-watermark\" (C2PA invisible\n watermark), \"chromaprint\" (audio fingerprint).").optional() }).describe("Resource identity for cross-exchange deduplication.\n Enables Brokers to recognize the same resource offered by\n different Exchanges and compare pricing.").optional(), "offer_id": z.string().describe("Unique identifier for this offer, assigned by the Exchange.\n Opaque to the caller: not derived from the resource, its URL, or any\n other field, and carries no meaning beyond identifying this offer.\n Two offers for the same resource have different offer_ids.").default(""), "previews": z.array(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("Preview — Lightweight resource preview for offer evaluation.\n\nThe Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no FORA transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")).describe("Lightweight previews for offer evaluation.\n The Exchange holds URLs (50–200 bytes each); the provider's CDN serves\n the actual bytes. Agents fetch previews only when evaluating offers —\n not on every discovery query. Multiple previews at different sizes\n allow agents to pick the cheapest fetch for their evaluation needs.\n\nPer content type:\n Image: watermarked thumbnail (150–450px JPEG)\n Video: short clip (10–30s MP4, watermarked)\n Audio: short clip (15–30s MP3, low-bitrate or watermarked)\n Text: snippet or abstract (first 200 words as text/plain)\n Data: sample records (1–3 rows as application/json)\n Stream: optional frame capture or none (streams are priced by time)\n\n Modeled after Shutterstock (multi-size thumbnail URLs),\n Spotify (preview_url to 30s clip), IIIF (parameterized image URLs),\n and OpenRTB native (img.url + dimensions).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("The offer's price, and its ONLY price. An offer represents a single\n licensing arrangement: each projected LicenseTerm yields its own offer, and\n the Exchange moves that term's price here, so the term inside `terms`\n carries no pricing of its own (the offer.terms.pricing_unset rule). This is\n the price execute charges, the price a Broker ranks and compares across\n Exchanges, and the price a metered purchase is charged at. A resource with\n multiple alternative terms (e.g. dual-licensed) produces multiple separate\n offers, one per term — never one offer with a \"headline\" picked among them.\n\nIt derives from exactly one catalog term: the term's pricing as the\n publisher declared it (ResourceEntry.terms). Each offer derives from one\n term, and the offer carries no second copy, so the offer's price and its\n term's price cannot disagree.\n\n On a metered (PER_UNIT) offer, the rate and the estimate a metered\n purchase is charged at are read from here (see Pricing). The estimate is\n optional. When the publisher states one — on the term's own pricing, or\n once for the resource on ResourceEntry.estimated_quantity — the Exchange\n carries it here as estimated_quantity, the term's own estimate taking\n precedence. When the publisher states none, the offer may carry none, and\n the purchase then charges one unit.").optional(), "reporting": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds). A report that arrives after\n the window closed is refused with USAGE_REPORT_REJECTION_REASON_WINDOW_EXPIRED\n (Connect code failed_precondition).").optional() }).describe("Post-usage reporting requirements for this offer.").optional(), "signature": z.string().describe("REQUIRED. Hex-encoded detached Ed25519 signature over the canonical\n serialization of the ENTIRE Offer — every field, including `pricing`,\n `terms` (the full licensing payload), `expires_at`, and `exchange`. Only\n `signature` and `signature_algorithm` are excluded from the signed bytes.\n `expires_at` is signed so the offer's validity window is\n integrity-protected: a relaying Broker cannot extend (or shorten) the TTL\n of a signed offer to replay it outside the window the Exchange intended.\n\nCANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n UNKNOWN FIELDS. A canonicalizer either OMITS content it has no schema for or\n PRESERVES it, and the rule follows from which:\n\n - OMITTING (e.g. proto-JSON, which emits only schema-defined fields): such a\n canonicalizer CANNOT reproduce the signed bytes of a message carrying\n unknown fields — what it renders silently drops part of what the signer\n covered. It MUST refuse the message rather than emit the reduced bytes,\n and a verifier built on it MUST reject rather than verify over them. The\n refusal binds at EVERY depth: a nested message and each element of a\n repeated or map field carries its own unknown-field set.\n - PRESERVING (a canonicalizer that carries unrecognized members through):\n it reproduces the signed bytes faithfully, so there is nothing to refuse.\n\n Either way an APPENDED field cannot pass: an omitting canonicalizer refuses\n the message, and a preserving one renders the appended member into bytes the\n signer never covered, so the signature fails. Without the refusal the omitting\n case would fail OPEN — an intermediary could add unknown fields to an\n already-signed message and leave its signature verifying, smuggling\n unauthenticated content through a message the recipient treats as verified.\n\n Extensions therefore ride in `ext` / `ext_critical`, which are defined fields\n and inside the signed bytes — never as undeclared field numbers.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.").default(""), "signature_algorithm": z.string().describe("JOSE/JWA algorithm identifier (RFC 8037 §3.1). Always 'EdDSA' for\n Ed25519. Advisory only: this field is cleared before the canonical\n payload is signed, so it is not covered by the signature.").default(""), "subscription_id": z.string().describe("If set, this offer is available under an existing subscription/deal.\n No per-request billing — usage tracked against subscription quota.\n Pricing.rate = \"0\" for subscription offers (zero marginal cost).\n The Broker SHOULD prefer subscription offers when available.").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("SubscriptionQuotaInfo — Proactive quota signaling for subscription access.\n\nAnalogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Subscription quota state, when this offer is under a subscription.\n Enables the agent to see remaining quota before committing.\n Multiple entries when the subscription has independent quotas\n (e.g., access count + spend cap).").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Obligation — A post-use behavioral requirement attached to a LicenseTerm.\n\nExamples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).max(64).describe("Post-use behavioral requirements.\n At most 64, for the reason quotas carries.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. Where it is required depends on the message that\n holds the term, so the requirement is a rule of that message rather than of\n this field:\n - On a catalog term (ResourceEntry.terms) it is REQUIRED regardless of\n semantics — an agent cannot act on a priceless term, so absent Pricing\n is a validation error at ingest (resource_entry.terms.pricing_required).\n model = FREE must be stated explicitly (absent Pricing is not free). A\n REFERENCE_ONLY term states its price here too; its License governs the\n human-readable terms but does not replace the machine-readable price.\n - On the term an offer carries (Offer.terms) it MUST be unset\n (offer.terms.pricing_unset): the offer's price is Offer.pricing, stated\n once.").optional(), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The unit being capped — an open vocabulary axis.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quota — A usage cap the term declares.\n\nA quota states how much a licensee may consume under the term before the\n term expires or must be renegotiated. Like a restriction, it is a declared\n term the parties agree to. Quotas are NOT billing quantities — billing is in\n Pricing.\n\n The metric vocabulary is authored ONLY in the (fora.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).max(64).describe("Usage caps. The agent must not exceed any individual Quota.\n At most 64, the bound every per-message list in this contract carries when\n no rule walks it more than once. It bounds what one term may carry, not the\n work of checking one — a validator walks every element it is handed before\n the cap is reported, so the cost of checking is bounded at the transport.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to. Defined-only: the axis set is\n CLOSED, and a number outside it is refused rather than ignored. A custom\n axis is RESTRICTION_KIND_OTHER, whose meaning rides in permitted/prohibited,\n so a new number was never the extension mechanism — accepting one would\n admit a restriction no consumer can evaluate onto a term whose default is\n BINDING (see advisory below), which fails open on the axis a publisher most\n needs honoured. Closing the axis does NOT bound the cost of the one-per-kind\n rule below, and must not be read as doing so: a number this rule refuses is\n still distinct from every other, so that rule's all() finds no duplicate to\n stop on and walks the list in full anyway. Its cost is bounded by the size\n test the rule itself carries."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restriction — A single constraint on one licensing dimension.\n\nRestrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: they are declared terms the parties agree to when the\n agent accepts the offer. The AGENT self-selects the term whose restrictions it\n can honour and bears compliance. The Exchange never enforces a restriction:\n it is NOT an Exchange-side gate the requester must pass to see or buy a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and by the scopes in the requester's signed\n request — see LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (fora.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — FORA user/organization categories")).max(8).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.\n At most 8, and this list is the one of the three that does NOT carry the\n contract's usual 64: only one restriction per axis is valid, the axis enum\n is defined-only, so four is the longest conformant list and eight leaves\n room for an axis this version does not have. Like the caps on quotas and\n obligations, this one bounds the DOCUMENT — how many restrictions one term\n may carry — and not the work of checking it: a validator walks every element\n it is handed before any cardinality rule is reported, so an over-cap list is\n traversed in full on its way to being refused.\n\nWhat makes this list different is that one rule walks it against ITSELF. The\n one-per-kind rule below is quadratic, so it carries its own size test and\n stays silent above this cap; a conformance guard holds the two numbers equal,\n because a cap raised without the test would leave the lists in between\n unchecked for duplicate axes and accepted. The neighbouring disjointness rule\n on each element is quadratic only in that element's two token lists, both\n capped at 64, so its cost is bounded per restriction and linear across the\n list — it needs no such test.").optional(), "scopes": z.array(z.string()).max(64).describe("Scope-gating: the Exchange presents an offer for this term only when the\n scopes in the requester's signed request (Requester.scopes) cover ALL of\n these scopes (AND-semantics). Empty = public. A subscription term is\n Pricing{model:FREE} + scopes:[\"subscription:...\"].\n\nRequester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n instead gate a term behind a verified delegation (a subscription term is\n the usual case): then only the delegation chain's verified effective scopes\n are matched against these scopes, and Requester.scopes is ignored for that\n term. To a requester that presents no valid delegation, it answers as if\n the term did not exist, as existence hiding requires (see\n Requester.scopes). An offer the Exchange presented is honoured until it\n expires, and a gated offer expires no later than the chain's effective\n expiry (see Delegation).\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments (at least one), and there is NO implicit prefix match\n (a grant narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("LicenseTerm — Universal licensing unit.\n\nOne LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation — with one move: on an offer the\n term's price is Offer.pricing, and the term itself carries none.\n\n Validation rules:\n - Pricing MUST be present on EVERY catalog term (ResourceEntry.terms),\n regardless of semantics — the resource_entry.terms.pricing_required\n rule. Absent Pricing → reject at ingest: an agent cannot act on a term\n with no price. This holds for REFERENCE_ONLY too — its License governs\n the human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - Pricing MUST be unset on the term an offer carries (Offer.terms) — the\n offer.terms.pricing_unset rule. The offer's price is Offer.pricing, and\n each offer derives from exactly one catalog term, so the price is stated\n once and cannot disagree with itself. The requirement is therefore a\n rule of the message that holds the term, not of LicenseTerm.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - A PER_UNIT term MAY state Pricing.estimated_quantity. It is not\n required, on the pushed term or on the offer it projects to — see\n Pricing.estimated_quantity.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).min(1).max(1).describe("The licensing term this offer sells, sourced from the publisher's\n ResourceEntry. EXACTLY ONE, and the bound is enforced rather than asked\n for: an offer IS a single licensing arrangement, so N terms on a resource\n project to N offers on that resource, each selling one of them — see\n `pricing` above. An offer with no term would be an offer projected from\n nothing, which is why the floor is one and not zero. The field is\n `repeated` because it shipped that way in v1 and renumbering a released\n field is not available to us, so the cardinality rides as a validation\n rule instead. `ResourceEntry.terms` is the plural side, bounded at 32: a\n resource carries many terms, an offer sells one of them.\n\nThe distinction is not cosmetic. `pricing` is what a Broker ranks on and\n what execute charges, so a term that is not on its own offer has no price\n a Broker can compare and no offer_id an agent can buy. Fusing several\n terms onto one offer makes every term but the first unsellable — a\n dual-licensed resource sells only under whichever arrangement the\n publisher happened to store first.\n\n The term here carries everything the publisher declared EXCEPT its price:\n its `pricing` MUST be unset (the offer.terms.pricing_unset rule), because\n the offer's price is `pricing` above. Its semantics, license,\n restrictions, quotas, obligations, scopes and part_label are the\n publisher's, unchanged.\n\n Where a term is reachable only under an existing subscription, its offer\n carries `subscription_id` and prices at zero marginal cost; the terms a\n requester may see at all are selected by `LicenseTerm.scopes`.\n\n See: Universal Licensing Core section.").optional(), "title": z.string().describe("Resource title (human-readable, for display/logging).").optional() }).describe("Offer — A single resource offer from an Exchange.\n\nCombines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via fora-comp-v1 extension profile.")).describe("Zero or more offers for this URI. Empty = resource not available.").optional(), "restriction_filters": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("When absence_reason = RESTRICTION_FILTERED, the restriction axes that drove\n the convenience pre-filter, in the same RestrictionKind vocabulary the terms\n use (e.g. [GEOGRAPHY] when the requester's stated geography matched no term).\n Advisory diagnostics, not an enforcement verdict.").optional(), "uri": z.string().describe("The URI this group of offers is for (echoed from ResourceQuery.uris).").default("") }).describe("OfferGroup — Offers for a single requested URI.\n Enables multi-URI batch queries where the caller needs to know\n which offers correspond to which requested resource.")).describe("Offers grouped by requested URI — the sole offer representation in this\n response. One OfferGroup per URI the agent asked for (echoed in\n OfferGroup.uri); a group with no offers carries OfferGroup.absence_reason\n explaining why. Each contained Offer is the full signed Offer the Exchange\n issued (including Offer.exchange, the execute-routing target), forwarded by\n the Broker unchanged so the agent can verify the signature end to end.").optional(), "ver": z.string().describe("FORA protocol version — \"1.0\". Stamped by the sender from a single\n constant; advisory on receive. See \"Protocol version\" in the file header.").default("") }).describe("DiscoveryResponse — Broker returns to Agent (Step 6).\n\nCarries discovery results only: the offers the Broker gathered across\n Exchanges, grouped by the URI they were requested for. Committing to an offer\n is a separate exchange on the execute path; that per-transaction result\n (transaction_id, billing_id, cost, delivery_method, retrieval endpoint, …)\n is returned by TransactionResponse, or by BrokerTransactionResponse when the\n purchase goes through BrokerService.ExecuteTransaction, not here.")))(); export const DisputeFailureSchema = /* @__PURE__ */ (() => wire(z.object({ "reason": z.enum(["DISPUTE_FAILURE_REASON_TRANSACTION_NOT_FOUND","DISPUTE_FAILURE_REASON_REPORT_NOT_FILED","DISPUTE_FAILURE_REASON_WINDOW_EXPIRED","DISPUTE_FAILURE_REASON_DUPLICATE","DISPUTE_FAILURE_REASON_INELIGIBLE","DISPUTE_FAILURE_REASON_UNKNOWN_CRITICAL_EXTENSION"]).describe("The failure reason (defined-only, non-zero)") }).describe("DisputeFailure — a dispute could not be filed.")))(); @@ -92,7 +92,7 @@ export const KeyRevocationListSchema = /* @__PURE__ */ (() => wire(z.object({ "a export const LicenseSchema = /* @__PURE__ */ (() => wire(z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("License — Identifies the governing license document for a LicenseTerm.")))(); -export const LicenseTermSchema = /* @__PURE__ */ (() => wire(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Obligation — A post-use behavioral requirement attached to a LicenseTerm.\n\nExamples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).max(64).describe("Post-use behavioral requirements.\n At most 64, for the reason quotas carries.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. Where it is required depends on the message that\n holds the term, so the requirement is a rule of that message rather than of\n this field:\n - On a catalog term (ResourceEntry.terms) it is REQUIRED regardless of\n semantics — an agent cannot act on a priceless term, so absent Pricing\n is a validation error at ingest (resource_entry.terms.pricing_required).\n model = FREE must be stated explicitly (absent Pricing is not free). A\n REFERENCE_ONLY term states its price here too; its License governs the\n human-readable terms but does not replace the machine-readable price.\n - On the term an offer carries (Offer.terms) it MUST be unset\n (offer.terms.pricing_unset): the offer's price is Offer.pricing, stated\n once.").optional(), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The unit being capped — an open vocabulary axis.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quota — A usage cap the term declares.\n\nA quota states how much a licensee may consume under the term before the\n term expires or must be renegotiated. Like a restriction, it is a declared\n term the parties agree to. Quotas are NOT billing quantities — billing is in\n Pricing.\n\n The metric vocabulary is authored ONLY in the (fora.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).max(64).describe("Usage caps. The agent must not exceed any individual Quota.\n At most 64, the bound every per-message list in this contract carries when\n no rule walks it more than once. It bounds what one term may carry, not the\n work of checking one — a validator walks every element it is handed before\n the cap is reported, so the cost of checking is bounded at the transport.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to. Defined-only: the axis set is\n CLOSED, and a number outside it is refused rather than ignored. A custom\n axis is RESTRICTION_KIND_OTHER, whose meaning rides in permitted/prohibited,\n so a new number was never the extension mechanism — accepting one would\n admit a restriction no consumer can evaluate onto a term whose default is\n BINDING (see advisory below), which fails open on the axis a publisher most\n needs honoured. Closing the axis does NOT bound the cost of the one-per-kind\n rule below, and must not be read as doing so: a number this rule refuses is\n still distinct from every other, so that rule's all() finds no duplicate to\n stop on and walks the list in full anyway. Its cost is bounded by the size\n test the rule itself carries."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restriction — A single constraint on one licensing dimension.\n\nRestrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: they are declared terms the parties agree to when the\n agent accepts the offer. The AGENT self-selects the term whose restrictions it\n can honour and bears compliance. The Exchange never enforces a restriction:\n it is NOT an Exchange-side gate the requester must pass to see or buy a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and by the scopes in the requester's signed\n request — see LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (fora.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — FORA user/organization categories")).max(8).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.\n At most 8, and this list is the one of the three that does NOT carry the\n contract's usual 64: only one restriction per axis is valid, the axis enum\n is defined-only, so four is the longest conformant list and eight leaves\n room for an axis this version does not have. Like the caps on quotas and\n obligations, this one bounds the DOCUMENT — how many restrictions one term\n may carry — and not the work of checking it: a validator walks every element\n it is handed before any cardinality rule is reported, so an over-cap list is\n traversed in full on its way to being refused.\n\nWhat makes this list different is that one rule walks it against ITSELF. The\n one-per-kind rule below is quadratic, so it carries its own size test and\n stays silent above this cap; a conformance guard holds the two numbers equal,\n because a cap raised without the test would leave the lists in between\n unchecked for duplicate axes and accepted. The neighbouring disjointness rule\n on each element is quadratic only in that element's two token lists, both\n capped at 64, so its cost is bounded per restriction and linear across the\n list — it needs no such test.").optional(), "scopes": z.array(z.string()).max(64).describe("Scope-gating: the Exchange presents an offer for this term only when the\n scopes in the requester's signed request (Requester.scopes) cover ALL of\n these scopes (AND-semantics). Empty = public. A subscription term is\n Pricing{model:FREE} + scopes:[\"subscription:...\"].\n\nRequester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n also require a verified delegation that grants these scopes before it\n presents the term. To a requester that presents none, it answers as if the\n term did not exist, as existence hiding requires (see Requester.scopes). An\n offer the Exchange presented is honoured until it expires.\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("LicenseTerm — Universal licensing unit.\n\nOne LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation — with one move: on an offer the\n term's price is Offer.pricing, and the term itself carries none.\n\n Validation rules:\n - Pricing MUST be present on EVERY catalog term (ResourceEntry.terms),\n regardless of semantics — the resource_entry.terms.pricing_required\n rule. Absent Pricing → reject at ingest: an agent cannot act on a term\n with no price. This holds for REFERENCE_ONLY too — its License governs\n the human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - Pricing MUST be unset on the term an offer carries (Offer.terms) — the\n offer.terms.pricing_unset rule. The offer's price is Offer.pricing, and\n each offer derives from exactly one catalog term, so the price is stated\n once and cannot disagree with itself. The requirement is therefore a\n rule of the message that holds the term, not of LicenseTerm.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - A PER_UNIT term MAY state Pricing.estimated_quantity. It is not\n required, on the pushed term or on the offer it projects to — see\n Pricing.estimated_quantity.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")))(); +export const LicenseTermSchema = /* @__PURE__ */ (() => wire(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Obligation — A post-use behavioral requirement attached to a LicenseTerm.\n\nExamples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).max(64).describe("Post-use behavioral requirements.\n At most 64, for the reason quotas carries.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. Where it is required depends on the message that\n holds the term, so the requirement is a rule of that message rather than of\n this field:\n - On a catalog term (ResourceEntry.terms) it is REQUIRED regardless of\n semantics — an agent cannot act on a priceless term, so absent Pricing\n is a validation error at ingest (resource_entry.terms.pricing_required).\n model = FREE must be stated explicitly (absent Pricing is not free). A\n REFERENCE_ONLY term states its price here too; its License governs the\n human-readable terms but does not replace the machine-readable price.\n - On the term an offer carries (Offer.terms) it MUST be unset\n (offer.terms.pricing_unset): the offer's price is Offer.pricing, stated\n once.").optional(), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The unit being capped — an open vocabulary axis.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quota — A usage cap the term declares.\n\nA quota states how much a licensee may consume under the term before the\n term expires or must be renegotiated. Like a restriction, it is a declared\n term the parties agree to. Quotas are NOT billing quantities — billing is in\n Pricing.\n\n The metric vocabulary is authored ONLY in the (fora.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).max(64).describe("Usage caps. The agent must not exceed any individual Quota.\n At most 64, the bound every per-message list in this contract carries when\n no rule walks it more than once. It bounds what one term may carry, not the\n work of checking one — a validator walks every element it is handed before\n the cap is reported, so the cost of checking is bounded at the transport.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to. Defined-only: the axis set is\n CLOSED, and a number outside it is refused rather than ignored. A custom\n axis is RESTRICTION_KIND_OTHER, whose meaning rides in permitted/prohibited,\n so a new number was never the extension mechanism — accepting one would\n admit a restriction no consumer can evaluate onto a term whose default is\n BINDING (see advisory below), which fails open on the axis a publisher most\n needs honoured. Closing the axis does NOT bound the cost of the one-per-kind\n rule below, and must not be read as doing so: a number this rule refuses is\n still distinct from every other, so that rule's all() finds no duplicate to\n stop on and walks the list in full anyway. Its cost is bounded by the size\n test the rule itself carries."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restriction — A single constraint on one licensing dimension.\n\nRestrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: they are declared terms the parties agree to when the\n agent accepts the offer. The AGENT self-selects the term whose restrictions it\n can honour and bears compliance. The Exchange never enforces a restriction:\n it is NOT an Exchange-side gate the requester must pass to see or buy a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and by the scopes in the requester's signed\n request — see LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (fora.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — FORA user/organization categories")).max(8).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.\n At most 8, and this list is the one of the three that does NOT carry the\n contract's usual 64: only one restriction per axis is valid, the axis enum\n is defined-only, so four is the longest conformant list and eight leaves\n room for an axis this version does not have. Like the caps on quotas and\n obligations, this one bounds the DOCUMENT — how many restrictions one term\n may carry — and not the work of checking it: a validator walks every element\n it is handed before any cardinality rule is reported, so an over-cap list is\n traversed in full on its way to being refused.\n\nWhat makes this list different is that one rule walks it against ITSELF. The\n one-per-kind rule below is quadratic, so it carries its own size test and\n stays silent above this cap; a conformance guard holds the two numbers equal,\n because a cap raised without the test would leave the lists in between\n unchecked for duplicate axes and accepted. The neighbouring disjointness rule\n on each element is quadratic only in that element's two token lists, both\n capped at 64, so its cost is bounded per restriction and linear across the\n list — it needs no such test.").optional(), "scopes": z.array(z.string()).max(64).describe("Scope-gating: the Exchange presents an offer for this term only when the\n scopes in the requester's signed request (Requester.scopes) cover ALL of\n these scopes (AND-semantics). Empty = public. A subscription term is\n Pricing{model:FREE} + scopes:[\"subscription:...\"].\n\nRequester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n instead gate a term behind a verified delegation (a subscription term is\n the usual case): then only the delegation chain's verified effective scopes\n are matched against these scopes, and Requester.scopes is ignored for that\n term. To a requester that presents no valid delegation, it answers as if\n the term did not exist, as existence hiding requires (see\n Requester.scopes). An offer the Exchange presented is honoured until it\n expires, and a gated offer expires no later than the chain's effective\n expiry (see Delegation).\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments (at least one), and there is NO implicit prefix match\n (a grant narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("LicenseTerm — Universal licensing unit.\n\nOne LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation — with one move: on an offer the\n term's price is Offer.pricing, and the term itself carries none.\n\n Validation rules:\n - Pricing MUST be present on EVERY catalog term (ResourceEntry.terms),\n regardless of semantics — the resource_entry.terms.pricing_required\n rule. Absent Pricing → reject at ingest: an agent cannot act on a term\n with no price. This holds for REFERENCE_ONLY too — its License governs\n the human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - Pricing MUST be unset on the term an offer carries (Offer.terms) — the\n offer.terms.pricing_unset rule. The offer's price is Offer.pricing, and\n each offer derives from exactly one catalog term, so the price is stated\n once and cannot disagree with itself. The requirement is therefore a\n rule of the message that holds the term, not of LicenseTerm.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - A PER_UNIT term MAY state Pricing.estimated_quantity. It is not\n required, on the pushed term or on the offer it projects to — see\n Pricing.estimated_quantity.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")))(); export const ObligationSchema = /* @__PURE__ */ (() => wire(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Obligation — A post-use behavioral requirement attached to a LicenseTerm.\n\nExamples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")))(); @@ -100,11 +100,11 @@ export const ObligationKindSchema = /* @__PURE__ */ (() => wire(z.enum(["OBLIGAT export const ObligationTriggerSchema = /* @__PURE__ */ (() => wire(z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"])))(); -export const OfferSchema = /* @__PURE__ */ (() => wire(z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"fora.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("ResourceAttestation — Signed envelope of claims from a trusted party.\n\nA provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"fora.attestation.claims_schema\"].")).describe("Signed attestations about the resource at this URI.\n Attestations provide cryptographic proof of\n resource properties from trusted parties (providers or verification vendors).\n\nThree verification levels determine what is independently verifiable:\n Level 0 (no attestations): Resource may carry identifiers (DOI, IPTC GUID)\n for identification, but nothing is cryptographically verifiable.\n Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): Provider signs own claims with Ed25519 key.\n Agent can independently verify content hash and token count.\n CDN delivery failure + content hash mismatch are auto-disputable.\n Level 2 (third-party attested): Independent verification vendor crawled\n the resource and attested to its properties. Agent trusts the attestation\n (does not re-verify hash). Token count discrepancy is auto-disputable\n when corroborated by CDN response size.\n\n Multiple attestations may be present (e.g., provider self-attestation\n plus a third-party verification). Agents choose which to trust.").optional(), "data_as_of": z.string().datetime({ offset: true }).describe("When the offered data was current. For dynamic resources\n (resource_mutability = DYNAMIC), this is the snapshot timestamp.\n Enables the Broker to evaluate freshness: \"this credit report\n reflects data as of March 18\" or \"this drug database was updated today.\"\n\nNot set for STATIC resources (content doesn't change) or LIVE\n resources (content doesn't exist yet).\n\n The Broker compares this against RequestConstraints.max_data_age\n to filter stale offers. Example: agent requests max_data_age = 7 days,\n Broker drops offers where now() - data_as_of > 7 days.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource will be delivered.").default(0), "exchange": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("REQUIRED. Bare host of the Exchange that issued this offer (e.g.\n \"exchange.example\" or \"exchange.example:8081\"), in the form \"Request\n recipient\" defines in the file header. This is the execute-routing target:\n the agent, or a relaying Broker, sends the ExecuteTransaction call for this\n offer to this Exchange, and a Broker relaying a mixed batch groups the items\n by this value. Because it is an ordinary Offer field it falls inside the\n signed bytes (see `signature` below — the signature covers every field\n except `signature` / `signature_algorithm`), so an intermediary cannot\n redirect the execute call to a different Exchange without invalidating the\n offer, and it is what retires the X-FORA-Exchange-Endpoint transport header.\n It is also the audience statement of an ExecuteTransaction, which is why\n TransactionRequest carries no top-level `exchange`: on receipt, an Exchange\n MUST reject the request unless EVERY item's offer.exchange names its own\n domain. Presence is enforced because an empty value is unroutable — a\n relaying Broker has nothing to group or dial on, and the swap-protection\n above is vacuous when the signed bytes carry no recipient at all."), "expires_at": z.string().datetime({ offset: true }).describe("When this offer expires (ISO 8601).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "iab_categories": z.array(z.string()).describe("IAB Content Taxonomy category codes.\n Enables agents to filter offers by topic (e.g., \"only finance resources\").\n Uses IAB Content Taxonomy 3.1 codes.").optional(), "identity": z.object({ "c2pa_manifest": z.string().describe("C2PA content credentials manifest URI.\n Points to a sidecar or embedded C2PA manifest for this resource.\n C2PA-aware agents MAY follow this URI to validate the full provenance\n chain (creator identity, transformation history, ingredient composition)\n using C2PA libraries (JUMBF/COSE Sign1). C2PA-unaware agents can rely\n on c2pa_status and c2pa-bridged attestation claims instead.\n\nFormats:\n Sidecar: HTTPS URI to a .c2pa manifest file\n Embedded: same URI as canonical_url (manifest is inside the asset)\n Content Credentials Cloud: https://contentcredentials.org/verify?uri=...").optional(), "c2pa_status": z.enum(["C2PA_STATUS_TRUSTED","C2PA_STATUS_VALID","C2PA_STATUS_INVALID","C2PA_STATUS_ABSENT"]).describe("The full C2PA validation details (signer identity, trust list,\n action history, training/mining status) are carried in a\n ResourceAttestation with c2pa.* claims — see fora-c2pa-v1 profile.").optional(), "canonical_url": z.string().describe("Provider's authoritative URL for this resource (rel=\"canonical\").\n Always available. Different per provider for syndicated content.").optional(), "content_hash": z.string().describe("Hash of the content. Interpretation depends on hash_method:\n \"simhash-v1\" → locality-sensitive hash, for fuzzy dedup (Level 1)\n \"sha256\" → exact-match integrity hash (Level 2)\n\nLevel 1 (SimHash): computed by Exchange from extracted text.\n Agent verifies that fetched content is \"substantially similar.\"\n Tolerates dynamic page elements.\n\n Level 2 (SHA-256): computed by provider from deterministic payload.\n Agent verifies exact match. Requires provider to serve consistent\n content (e.g., API endpoint, static HTML, structured JSON).\n Mismatch = dispute. Commands premium pricing.").optional(), "doi": z.string().describe("Digital Object Identifier — persistent, never changes.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().describe("Hash algorithm and verification level.\n Examples: \"simhash-v1\", \"minhash-v1\", \"sha256\", \"sha384\"").optional(), "iptc_guid": z.string().describe("IPTC NewsML-G2 globally unique identifier.\n Present when resource flows through news wire syndication (AP, Reuters).").optional(), "isni": z.string().describe("International Standard Name Identifier for the creator.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Drives hash verification behavior:\n STATIC: content_hash is stable. Agent SHOULD verify delivered content matches.\n DYNAMIC: content changes between offer and fetch (credit reports, drug databases).\n content_hash reflects state at offer generation time. Hash mismatch is\n expected and MUST NOT trigger automatic dispute.\n LIVE: content does not exist at offer time (streaming feeds, live broadcasts).\n content_hash is not applicable. The \"resource\" is the stream endpoint.\n\n Validated across 18 use cases: static content (articles, patents, legislation),\n dynamic data (credit reports, drug interactions, stock snapshots), and live\n streams (MarketData quotes, NPR broadcast, news monitoring feeds)."), "soft_binding": z.string().describe("Soft binding hash — content-derived identifier that survives format\n transcoding (resolution changes, compression, PDF-to-text extraction).\n Extracted from C2PA soft binding assertion when present.\n Enables post-delivery verification when the hard binding hash breaks\n due to legitimate format conversion.\n\nAlgorithm specified in soft_binding_method. Values are algorithm-specific\n (e.g., perceptual hash hex string, watermark identifier).").optional(), "soft_binding_method": z.string().describe("Algorithm used for soft_binding.\n Examples: \"phash-v1\" (perceptual hash), \"c2pa-watermark\" (C2PA invisible\n watermark), \"chromaprint\" (audio fingerprint).").optional() }).describe("Resource identity for cross-exchange deduplication.\n Enables Brokers to recognize the same resource offered by\n different Exchanges and compare pricing.").optional(), "offer_id": z.string().describe("Unique identifier for this offer, assigned by the Exchange.\n Opaque to the caller: not derived from the resource, its URL, or any\n other field, and carries no meaning beyond identifying this offer.\n Two offers for the same resource have different offer_ids.").default(""), "previews": z.array(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("Preview — Lightweight resource preview for offer evaluation.\n\nThe Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no FORA transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")).describe("Lightweight previews for offer evaluation.\n The Exchange holds URLs (50–200 bytes each); the provider's CDN serves\n the actual bytes. Agents fetch previews only when evaluating offers —\n not on every discovery query. Multiple previews at different sizes\n allow agents to pick the cheapest fetch for their evaluation needs.\n\nPer content type:\n Image: watermarked thumbnail (150–450px JPEG)\n Video: short clip (10–30s MP4, watermarked)\n Audio: short clip (15–30s MP3, low-bitrate or watermarked)\n Text: snippet or abstract (first 200 words as text/plain)\n Data: sample records (1–3 rows as application/json)\n Stream: optional frame capture or none (streams are priced by time)\n\n Modeled after Shutterstock (multi-size thumbnail URLs),\n Spotify (preview_url to 30s clip), IIIF (parameterized image URLs),\n and OpenRTB native (img.url + dimensions).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("The offer's price, and its ONLY price. An offer represents a single\n licensing arrangement: each projected LicenseTerm yields its own offer, and\n the Exchange moves that term's price here, so the term inside `terms`\n carries no pricing of its own (the offer.terms.pricing_unset rule). This is\n the price execute charges, the price a Broker ranks and compares across\n Exchanges, and the price a metered purchase is charged at. A resource with\n multiple alternative terms (e.g. dual-licensed) produces multiple separate\n offers, one per term — never one offer with a \"headline\" picked among them.\n\nIt derives from exactly one catalog term: the term's pricing as the\n publisher declared it (ResourceEntry.terms). Each offer derives from one\n term, and the offer carries no second copy, so the offer's price and its\n term's price cannot disagree.\n\n On a metered (PER_UNIT) offer, the rate and the estimate a metered\n purchase is charged at are read from here (see Pricing). The estimate is\n optional. When the publisher states one — on the term's own pricing, or\n once for the resource on ResourceEntry.estimated_quantity — the Exchange\n carries it here as estimated_quantity, the term's own estimate taking\n precedence. When the publisher states none, the offer may carry none, and\n the purchase then charges one unit.").optional(), "reporting": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds). A report that arrives after\n the window closed is refused with USAGE_REPORT_REJECTION_REASON_WINDOW_EXPIRED\n (Connect code failed_precondition).").optional() }).describe("Post-usage reporting requirements for this offer.").optional(), "signature": z.string().describe("REQUIRED. Hex-encoded detached Ed25519 signature over the canonical\n serialization of the ENTIRE Offer — every field, including `pricing`,\n `terms` (the full licensing payload), `expires_at`, and `exchange`. Only\n `signature` and `signature_algorithm` are excluded from the signed bytes.\n `expires_at` is signed so the offer's validity window is\n integrity-protected: a relaying Broker cannot extend (or shorten) the TTL\n of a signed offer to replay it outside the window the Exchange intended.\n\nCANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n UNKNOWN FIELDS. A canonicalizer either OMITS content it has no schema for or\n PRESERVES it, and the rule follows from which:\n\n - OMITTING (e.g. proto-JSON, which emits only schema-defined fields): such a\n canonicalizer CANNOT reproduce the signed bytes of a message carrying\n unknown fields — what it renders silently drops part of what the signer\n covered. It MUST refuse the message rather than emit the reduced bytes,\n and a verifier built on it MUST reject rather than verify over them. The\n refusal binds at EVERY depth: a nested message and each element of a\n repeated or map field carries its own unknown-field set.\n - PRESERVING (a canonicalizer that carries unrecognized members through):\n it reproduces the signed bytes faithfully, so there is nothing to refuse.\n\n Either way an APPENDED field cannot pass: an omitting canonicalizer refuses\n the message, and a preserving one renders the appended member into bytes the\n signer never covered, so the signature fails. Without the refusal the omitting\n case would fail OPEN — an intermediary could add unknown fields to an\n already-signed message and leave its signature verifying, smuggling\n unauthenticated content through a message the recipient treats as verified.\n\n Extensions therefore ride in `ext` / `ext_critical`, which are defined fields\n and inside the signed bytes — never as undeclared field numbers.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.").default(""), "signature_algorithm": z.string().describe("JOSE/JWA algorithm identifier (RFC 8037 §3.1). Always 'EdDSA' for\n Ed25519. Advisory only: this field is cleared before the canonical\n payload is signed, so it is not covered by the signature.").default(""), "subscription_id": z.string().describe("If set, this offer is available under an existing subscription/deal.\n No per-request billing — usage tracked against subscription quota.\n Pricing.rate = \"0\" for subscription offers (zero marginal cost).\n The Broker SHOULD prefer subscription offers when available.").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("SubscriptionQuotaInfo — Proactive quota signaling for subscription access.\n\nAnalogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Subscription quota state, when this offer is under a subscription.\n Enables the agent to see remaining quota before committing.\n Multiple entries when the subscription has independent quotas\n (e.g., access count + spend cap).").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Obligation — A post-use behavioral requirement attached to a LicenseTerm.\n\nExamples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).max(64).describe("Post-use behavioral requirements.\n At most 64, for the reason quotas carries.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. Where it is required depends on the message that\n holds the term, so the requirement is a rule of that message rather than of\n this field:\n - On a catalog term (ResourceEntry.terms) it is REQUIRED regardless of\n semantics — an agent cannot act on a priceless term, so absent Pricing\n is a validation error at ingest (resource_entry.terms.pricing_required).\n model = FREE must be stated explicitly (absent Pricing is not free). A\n REFERENCE_ONLY term states its price here too; its License governs the\n human-readable terms but does not replace the machine-readable price.\n - On the term an offer carries (Offer.terms) it MUST be unset\n (offer.terms.pricing_unset): the offer's price is Offer.pricing, stated\n once.").optional(), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The unit being capped — an open vocabulary axis.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quota — A usage cap the term declares.\n\nA quota states how much a licensee may consume under the term before the\n term expires or must be renegotiated. Like a restriction, it is a declared\n term the parties agree to. Quotas are NOT billing quantities — billing is in\n Pricing.\n\n The metric vocabulary is authored ONLY in the (fora.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).max(64).describe("Usage caps. The agent must not exceed any individual Quota.\n At most 64, the bound every per-message list in this contract carries when\n no rule walks it more than once. It bounds what one term may carry, not the\n work of checking one — a validator walks every element it is handed before\n the cap is reported, so the cost of checking is bounded at the transport.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to. Defined-only: the axis set is\n CLOSED, and a number outside it is refused rather than ignored. A custom\n axis is RESTRICTION_KIND_OTHER, whose meaning rides in permitted/prohibited,\n so a new number was never the extension mechanism — accepting one would\n admit a restriction no consumer can evaluate onto a term whose default is\n BINDING (see advisory below), which fails open on the axis a publisher most\n needs honoured. Closing the axis does NOT bound the cost of the one-per-kind\n rule below, and must not be read as doing so: a number this rule refuses is\n still distinct from every other, so that rule's all() finds no duplicate to\n stop on and walks the list in full anyway. Its cost is bounded by the size\n test the rule itself carries."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restriction — A single constraint on one licensing dimension.\n\nRestrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: they are declared terms the parties agree to when the\n agent accepts the offer. The AGENT self-selects the term whose restrictions it\n can honour and bears compliance. The Exchange never enforces a restriction:\n it is NOT an Exchange-side gate the requester must pass to see or buy a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and by the scopes in the requester's signed\n request — see LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (fora.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — FORA user/organization categories")).max(8).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.\n At most 8, and this list is the one of the three that does NOT carry the\n contract's usual 64: only one restriction per axis is valid, the axis enum\n is defined-only, so four is the longest conformant list and eight leaves\n room for an axis this version does not have. Like the caps on quotas and\n obligations, this one bounds the DOCUMENT — how many restrictions one term\n may carry — and not the work of checking it: a validator walks every element\n it is handed before any cardinality rule is reported, so an over-cap list is\n traversed in full on its way to being refused.\n\nWhat makes this list different is that one rule walks it against ITSELF. The\n one-per-kind rule below is quadratic, so it carries its own size test and\n stays silent above this cap; a conformance guard holds the two numbers equal,\n because a cap raised without the test would leave the lists in between\n unchecked for duplicate axes and accepted. The neighbouring disjointness rule\n on each element is quadratic only in that element's two token lists, both\n capped at 64, so its cost is bounded per restriction and linear across the\n list — it needs no such test.").optional(), "scopes": z.array(z.string()).max(64).describe("Scope-gating: the Exchange presents an offer for this term only when the\n scopes in the requester's signed request (Requester.scopes) cover ALL of\n these scopes (AND-semantics). Empty = public. A subscription term is\n Pricing{model:FREE} + scopes:[\"subscription:...\"].\n\nRequester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n also require a verified delegation that grants these scopes before it\n presents the term. To a requester that presents none, it answers as if the\n term did not exist, as existence hiding requires (see Requester.scopes). An\n offer the Exchange presented is honoured until it expires.\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("LicenseTerm — Universal licensing unit.\n\nOne LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation — with one move: on an offer the\n term's price is Offer.pricing, and the term itself carries none.\n\n Validation rules:\n - Pricing MUST be present on EVERY catalog term (ResourceEntry.terms),\n regardless of semantics — the resource_entry.terms.pricing_required\n rule. Absent Pricing → reject at ingest: an agent cannot act on a term\n with no price. This holds for REFERENCE_ONLY too — its License governs\n the human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - Pricing MUST be unset on the term an offer carries (Offer.terms) — the\n offer.terms.pricing_unset rule. The offer's price is Offer.pricing, and\n each offer derives from exactly one catalog term, so the price is stated\n once and cannot disagree with itself. The requirement is therefore a\n rule of the message that holds the term, not of LicenseTerm.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - A PER_UNIT term MAY state Pricing.estimated_quantity. It is not\n required, on the pushed term or on the offer it projects to — see\n Pricing.estimated_quantity.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).min(1).max(1).describe("The licensing term this offer sells, sourced from the publisher's\n ResourceEntry. EXACTLY ONE, and the bound is enforced rather than asked\n for: an offer IS a single licensing arrangement, so N terms on a resource\n project to N offers on that resource, each selling one of them — see\n `pricing` above. An offer with no term would be an offer projected from\n nothing, which is why the floor is one and not zero. The field is\n `repeated` because it shipped that way in v1 and renumbering a released\n field is not available to us, so the cardinality rides as a validation\n rule instead. `ResourceEntry.terms` is the plural side, bounded at 32: a\n resource carries many terms, an offer sells one of them.\n\nThe distinction is not cosmetic. `pricing` is what a Broker ranks on and\n what execute charges, so a term that is not on its own offer has no price\n a Broker can compare and no offer_id an agent can buy. Fusing several\n terms onto one offer makes every term but the first unsellable — a\n dual-licensed resource sells only under whichever arrangement the\n publisher happened to store first.\n\n The term here carries everything the publisher declared EXCEPT its price:\n its `pricing` MUST be unset (the offer.terms.pricing_unset rule), because\n the offer's price is `pricing` above. Its semantics, license,\n restrictions, quotas, obligations, scopes and part_label are the\n publisher's, unchanged.\n\n Where a term is reachable only under an existing subscription, its offer\n carries `subscription_id` and prices at zero marginal cost; the terms a\n requester may see at all are selected by `LicenseTerm.scopes`.\n\n See: Universal Licensing Core section.").optional(), "title": z.string().describe("Resource title (human-readable, for display/logging).").optional() }).describe("Offer — A single resource offer from an Exchange.\n\nCombines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via fora-comp-v1 extension profile.")))(); +export const OfferSchema = /* @__PURE__ */ (() => wire(z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"fora.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("ResourceAttestation — Signed envelope of claims from a trusted party.\n\nA provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"fora.attestation.claims_schema\"].")).describe("Signed attestations about the resource at this URI.\n Attestations provide cryptographic proof of\n resource properties from trusted parties (providers or verification vendors).\n\nThree verification levels determine what is independently verifiable:\n Level 0 (no attestations): Resource may carry identifiers (DOI, IPTC GUID)\n for identification, but nothing is cryptographically verifiable.\n Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): Provider signs own claims with Ed25519 key.\n Agent can independently verify content hash and token count.\n CDN delivery failure + content hash mismatch are auto-disputable.\n Level 2 (third-party attested): Independent verification vendor crawled\n the resource and attested to its properties. Agent trusts the attestation\n (does not re-verify hash). Token count discrepancy is auto-disputable\n when corroborated by CDN response size.\n\n Multiple attestations may be present (e.g., provider self-attestation\n plus a third-party verification). Agents choose which to trust.").optional(), "data_as_of": z.string().datetime({ offset: true }).describe("When the offered data was current. For dynamic resources\n (resource_mutability = DYNAMIC), this is the snapshot timestamp.\n Enables the Broker to evaluate freshness: \"this credit report\n reflects data as of March 18\" or \"this drug database was updated today.\"\n\nNot set for STATIC resources (content doesn't change) or LIVE\n resources (content doesn't exist yet).\n\n The Broker compares this against RequestConstraints.max_data_age\n to filter stale offers. Example: agent requests max_data_age = 7 days,\n Broker drops offers where now() - data_as_of > 7 days.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource will be delivered.").default(0), "exchange": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("REQUIRED. Bare host of the Exchange that issued this offer (e.g.\n \"exchange.example\" or \"exchange.example:8081\"), in the form \"Request\n recipient\" defines in the file header. This is the execute-routing target:\n the agent, or a relaying Broker, sends the ExecuteTransaction call for this\n offer to this Exchange, and a Broker relaying a mixed batch groups the items\n by this value. Because it is an ordinary Offer field it falls inside the\n signed bytes (see `signature` below — the signature covers every field\n except `signature` / `signature_algorithm`), so an intermediary cannot\n redirect the execute call to a different Exchange without invalidating the\n offer, and it is what retires the X-FORA-Exchange-Endpoint transport header.\n It is also the audience statement of an ExecuteTransaction, which is why\n TransactionRequest carries no top-level `exchange`: on receipt, an Exchange\n MUST reject the request unless EVERY item's offer.exchange names its own\n domain. Presence is enforced because an empty value is unroutable — a\n relaying Broker has nothing to group or dial on, and the swap-protection\n above is vacuous when the signed bytes carry no recipient at all."), "expires_at": z.string().datetime({ offset: true }).describe("When this offer expires (ISO 8601). An offer for a term gated behind a\n verified delegation MUST NOT expire later than that delegation chain's\n effective expiry, the earliest `exp` in the chain (see Delegation).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "iab_categories": z.array(z.string()).describe("IAB Content Taxonomy category codes.\n Enables agents to filter offers by topic (e.g., \"only finance resources\").\n Uses IAB Content Taxonomy 3.1 codes.").optional(), "identity": z.object({ "c2pa_manifest": z.string().describe("C2PA content credentials manifest URI.\n Points to a sidecar or embedded C2PA manifest for this resource.\n C2PA-aware agents MAY follow this URI to validate the full provenance\n chain (creator identity, transformation history, ingredient composition)\n using C2PA libraries (JUMBF/COSE Sign1). C2PA-unaware agents can rely\n on c2pa_status and c2pa-bridged attestation claims instead.\n\nFormats:\n Sidecar: HTTPS URI to a .c2pa manifest file\n Embedded: same URI as canonical_url (manifest is inside the asset)\n Content Credentials Cloud: https://contentcredentials.org/verify?uri=...").optional(), "c2pa_status": z.enum(["C2PA_STATUS_TRUSTED","C2PA_STATUS_VALID","C2PA_STATUS_INVALID","C2PA_STATUS_ABSENT"]).describe("The full C2PA validation details (signer identity, trust list,\n action history, training/mining status) are carried in a\n ResourceAttestation with c2pa.* claims — see fora-c2pa-v1 profile.").optional(), "canonical_url": z.string().describe("Provider's authoritative URL for this resource (rel=\"canonical\").\n Always available. Different per provider for syndicated content.").optional(), "content_hash": z.string().describe("Hash of the content. Interpretation depends on hash_method:\n \"simhash-v1\" → locality-sensitive hash, for fuzzy dedup (Level 1)\n \"sha256\" → exact-match integrity hash (Level 2)\n\nLevel 1 (SimHash): computed by Exchange from extracted text.\n Agent verifies that fetched content is \"substantially similar.\"\n Tolerates dynamic page elements.\n\n Level 2 (SHA-256): computed by provider from deterministic payload.\n Agent verifies exact match. Requires provider to serve consistent\n content (e.g., API endpoint, static HTML, structured JSON).\n Mismatch = dispute. Commands premium pricing.").optional(), "doi": z.string().describe("Digital Object Identifier — persistent, never changes.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().describe("Hash algorithm and verification level.\n Examples: \"simhash-v1\", \"minhash-v1\", \"sha256\", \"sha384\"").optional(), "iptc_guid": z.string().describe("IPTC NewsML-G2 globally unique identifier.\n Present when resource flows through news wire syndication (AP, Reuters).").optional(), "isni": z.string().describe("International Standard Name Identifier for the creator.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Drives hash verification behavior:\n STATIC: content_hash is stable. Agent SHOULD verify delivered content matches.\n DYNAMIC: content changes between offer and fetch (credit reports, drug databases).\n content_hash reflects state at offer generation time. Hash mismatch is\n expected and MUST NOT trigger automatic dispute.\n LIVE: content does not exist at offer time (streaming feeds, live broadcasts).\n content_hash is not applicable. The \"resource\" is the stream endpoint.\n\n Validated across 18 use cases: static content (articles, patents, legislation),\n dynamic data (credit reports, drug interactions, stock snapshots), and live\n streams (MarketData quotes, NPR broadcast, news monitoring feeds)."), "soft_binding": z.string().describe("Soft binding hash — content-derived identifier that survives format\n transcoding (resolution changes, compression, PDF-to-text extraction).\n Extracted from C2PA soft binding assertion when present.\n Enables post-delivery verification when the hard binding hash breaks\n due to legitimate format conversion.\n\nAlgorithm specified in soft_binding_method. Values are algorithm-specific\n (e.g., perceptual hash hex string, watermark identifier).").optional(), "soft_binding_method": z.string().describe("Algorithm used for soft_binding.\n Examples: \"phash-v1\" (perceptual hash), \"c2pa-watermark\" (C2PA invisible\n watermark), \"chromaprint\" (audio fingerprint).").optional() }).describe("Resource identity for cross-exchange deduplication.\n Enables Brokers to recognize the same resource offered by\n different Exchanges and compare pricing.").optional(), "offer_id": z.string().describe("Unique identifier for this offer, assigned by the Exchange.\n Opaque to the caller: not derived from the resource, its URL, or any\n other field, and carries no meaning beyond identifying this offer.\n Two offers for the same resource have different offer_ids.").default(""), "previews": z.array(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("Preview — Lightweight resource preview for offer evaluation.\n\nThe Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no FORA transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")).describe("Lightweight previews for offer evaluation.\n The Exchange holds URLs (50–200 bytes each); the provider's CDN serves\n the actual bytes. Agents fetch previews only when evaluating offers —\n not on every discovery query. Multiple previews at different sizes\n allow agents to pick the cheapest fetch for their evaluation needs.\n\nPer content type:\n Image: watermarked thumbnail (150–450px JPEG)\n Video: short clip (10–30s MP4, watermarked)\n Audio: short clip (15–30s MP3, low-bitrate or watermarked)\n Text: snippet or abstract (first 200 words as text/plain)\n Data: sample records (1–3 rows as application/json)\n Stream: optional frame capture or none (streams are priced by time)\n\n Modeled after Shutterstock (multi-size thumbnail URLs),\n Spotify (preview_url to 30s clip), IIIF (parameterized image URLs),\n and OpenRTB native (img.url + dimensions).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("The offer's price, and its ONLY price. An offer represents a single\n licensing arrangement: each projected LicenseTerm yields its own offer, and\n the Exchange moves that term's price here, so the term inside `terms`\n carries no pricing of its own (the offer.terms.pricing_unset rule). This is\n the price execute charges, the price a Broker ranks and compares across\n Exchanges, and the price a metered purchase is charged at. A resource with\n multiple alternative terms (e.g. dual-licensed) produces multiple separate\n offers, one per term — never one offer with a \"headline\" picked among them.\n\nIt derives from exactly one catalog term: the term's pricing as the\n publisher declared it (ResourceEntry.terms). Each offer derives from one\n term, and the offer carries no second copy, so the offer's price and its\n term's price cannot disagree.\n\n On a metered (PER_UNIT) offer, the rate and the estimate a metered\n purchase is charged at are read from here (see Pricing). The estimate is\n optional. When the publisher states one — on the term's own pricing, or\n once for the resource on ResourceEntry.estimated_quantity — the Exchange\n carries it here as estimated_quantity, the term's own estimate taking\n precedence. When the publisher states none, the offer may carry none, and\n the purchase then charges one unit.").optional(), "reporting": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds). A report that arrives after\n the window closed is refused with USAGE_REPORT_REJECTION_REASON_WINDOW_EXPIRED\n (Connect code failed_precondition).").optional() }).describe("Post-usage reporting requirements for this offer.").optional(), "signature": z.string().describe("REQUIRED. Hex-encoded detached Ed25519 signature over the canonical\n serialization of the ENTIRE Offer — every field, including `pricing`,\n `terms` (the full licensing payload), `expires_at`, and `exchange`. Only\n `signature` and `signature_algorithm` are excluded from the signed bytes.\n `expires_at` is signed so the offer's validity window is\n integrity-protected: a relaying Broker cannot extend (or shorten) the TTL\n of a signed offer to replay it outside the window the Exchange intended.\n\nCANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n UNKNOWN FIELDS. A canonicalizer either OMITS content it has no schema for or\n PRESERVES it, and the rule follows from which:\n\n - OMITTING (e.g. proto-JSON, which emits only schema-defined fields): such a\n canonicalizer CANNOT reproduce the signed bytes of a message carrying\n unknown fields — what it renders silently drops part of what the signer\n covered. It MUST refuse the message rather than emit the reduced bytes,\n and a verifier built on it MUST reject rather than verify over them. The\n refusal binds at EVERY depth: a nested message and each element of a\n repeated or map field carries its own unknown-field set.\n - PRESERVING (a canonicalizer that carries unrecognized members through):\n it reproduces the signed bytes faithfully, so there is nothing to refuse.\n\n Either way an APPENDED field cannot pass: an omitting canonicalizer refuses\n the message, and a preserving one renders the appended member into bytes the\n signer never covered, so the signature fails. Without the refusal the omitting\n case would fail OPEN — an intermediary could add unknown fields to an\n already-signed message and leave its signature verifying, smuggling\n unauthenticated content through a message the recipient treats as verified.\n\n Extensions therefore ride in `ext` / `ext_critical`, which are defined fields\n and inside the signed bytes — never as undeclared field numbers.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.").default(""), "signature_algorithm": z.string().describe("JOSE/JWA algorithm identifier (RFC 8037 §3.1). Always 'EdDSA' for\n Ed25519. Advisory only: this field is cleared before the canonical\n payload is signed, so it is not covered by the signature.").default(""), "subscription_id": z.string().describe("If set, this offer is available under an existing subscription/deal.\n No per-request billing — usage tracked against subscription quota.\n Pricing.rate = \"0\" for subscription offers (zero marginal cost).\n The Broker SHOULD prefer subscription offers when available.").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("SubscriptionQuotaInfo — Proactive quota signaling for subscription access.\n\nAnalogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Subscription quota state, when this offer is under a subscription.\n Enables the agent to see remaining quota before committing.\n Multiple entries when the subscription has independent quotas\n (e.g., access count + spend cap).").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Obligation — A post-use behavioral requirement attached to a LicenseTerm.\n\nExamples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).max(64).describe("Post-use behavioral requirements.\n At most 64, for the reason quotas carries.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. Where it is required depends on the message that\n holds the term, so the requirement is a rule of that message rather than of\n this field:\n - On a catalog term (ResourceEntry.terms) it is REQUIRED regardless of\n semantics — an agent cannot act on a priceless term, so absent Pricing\n is a validation error at ingest (resource_entry.terms.pricing_required).\n model = FREE must be stated explicitly (absent Pricing is not free). A\n REFERENCE_ONLY term states its price here too; its License governs the\n human-readable terms but does not replace the machine-readable price.\n - On the term an offer carries (Offer.terms) it MUST be unset\n (offer.terms.pricing_unset): the offer's price is Offer.pricing, stated\n once.").optional(), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The unit being capped — an open vocabulary axis.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quota — A usage cap the term declares.\n\nA quota states how much a licensee may consume under the term before the\n term expires or must be renegotiated. Like a restriction, it is a declared\n term the parties agree to. Quotas are NOT billing quantities — billing is in\n Pricing.\n\n The metric vocabulary is authored ONLY in the (fora.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).max(64).describe("Usage caps. The agent must not exceed any individual Quota.\n At most 64, the bound every per-message list in this contract carries when\n no rule walks it more than once. It bounds what one term may carry, not the\n work of checking one — a validator walks every element it is handed before\n the cap is reported, so the cost of checking is bounded at the transport.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to. Defined-only: the axis set is\n CLOSED, and a number outside it is refused rather than ignored. A custom\n axis is RESTRICTION_KIND_OTHER, whose meaning rides in permitted/prohibited,\n so a new number was never the extension mechanism — accepting one would\n admit a restriction no consumer can evaluate onto a term whose default is\n BINDING (see advisory below), which fails open on the axis a publisher most\n needs honoured. Closing the axis does NOT bound the cost of the one-per-kind\n rule below, and must not be read as doing so: a number this rule refuses is\n still distinct from every other, so that rule's all() finds no duplicate to\n stop on and walks the list in full anyway. Its cost is bounded by the size\n test the rule itself carries."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restriction — A single constraint on one licensing dimension.\n\nRestrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: they are declared terms the parties agree to when the\n agent accepts the offer. The AGENT self-selects the term whose restrictions it\n can honour and bears compliance. The Exchange never enforces a restriction:\n it is NOT an Exchange-side gate the requester must pass to see or buy a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and by the scopes in the requester's signed\n request — see LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (fora.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — FORA user/organization categories")).max(8).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.\n At most 8, and this list is the one of the three that does NOT carry the\n contract's usual 64: only one restriction per axis is valid, the axis enum\n is defined-only, so four is the longest conformant list and eight leaves\n room for an axis this version does not have. Like the caps on quotas and\n obligations, this one bounds the DOCUMENT — how many restrictions one term\n may carry — and not the work of checking it: a validator walks every element\n it is handed before any cardinality rule is reported, so an over-cap list is\n traversed in full on its way to being refused.\n\nWhat makes this list different is that one rule walks it against ITSELF. The\n one-per-kind rule below is quadratic, so it carries its own size test and\n stays silent above this cap; a conformance guard holds the two numbers equal,\n because a cap raised without the test would leave the lists in between\n unchecked for duplicate axes and accepted. The neighbouring disjointness rule\n on each element is quadratic only in that element's two token lists, both\n capped at 64, so its cost is bounded per restriction and linear across the\n list — it needs no such test.").optional(), "scopes": z.array(z.string()).max(64).describe("Scope-gating: the Exchange presents an offer for this term only when the\n scopes in the requester's signed request (Requester.scopes) cover ALL of\n these scopes (AND-semantics). Empty = public. A subscription term is\n Pricing{model:FREE} + scopes:[\"subscription:...\"].\n\nRequester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n instead gate a term behind a verified delegation (a subscription term is\n the usual case): then only the delegation chain's verified effective scopes\n are matched against these scopes, and Requester.scopes is ignored for that\n term. To a requester that presents no valid delegation, it answers as if\n the term did not exist, as existence hiding requires (see\n Requester.scopes). An offer the Exchange presented is honoured until it\n expires, and a gated offer expires no later than the chain's effective\n expiry (see Delegation).\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments (at least one), and there is NO implicit prefix match\n (a grant narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("LicenseTerm — Universal licensing unit.\n\nOne LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation — with one move: on an offer the\n term's price is Offer.pricing, and the term itself carries none.\n\n Validation rules:\n - Pricing MUST be present on EVERY catalog term (ResourceEntry.terms),\n regardless of semantics — the resource_entry.terms.pricing_required\n rule. Absent Pricing → reject at ingest: an agent cannot act on a term\n with no price. This holds for REFERENCE_ONLY too — its License governs\n the human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - Pricing MUST be unset on the term an offer carries (Offer.terms) — the\n offer.terms.pricing_unset rule. The offer's price is Offer.pricing, and\n each offer derives from exactly one catalog term, so the price is stated\n once and cannot disagree with itself. The requirement is therefore a\n rule of the message that holds the term, not of LicenseTerm.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - A PER_UNIT term MAY state Pricing.estimated_quantity. It is not\n required, on the pushed term or on the offer it projects to — see\n Pricing.estimated_quantity.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).min(1).max(1).describe("The licensing term this offer sells, sourced from the publisher's\n ResourceEntry. EXACTLY ONE, and the bound is enforced rather than asked\n for: an offer IS a single licensing arrangement, so N terms on a resource\n project to N offers on that resource, each selling one of them — see\n `pricing` above. An offer with no term would be an offer projected from\n nothing, which is why the floor is one and not zero. The field is\n `repeated` because it shipped that way in v1 and renumbering a released\n field is not available to us, so the cardinality rides as a validation\n rule instead. `ResourceEntry.terms` is the plural side, bounded at 32: a\n resource carries many terms, an offer sells one of them.\n\nThe distinction is not cosmetic. `pricing` is what a Broker ranks on and\n what execute charges, so a term that is not on its own offer has no price\n a Broker can compare and no offer_id an agent can buy. Fusing several\n terms onto one offer makes every term but the first unsellable — a\n dual-licensed resource sells only under whichever arrangement the\n publisher happened to store first.\n\n The term here carries everything the publisher declared EXCEPT its price:\n its `pricing` MUST be unset (the offer.terms.pricing_unset rule), because\n the offer's price is `pricing` above. Its semantics, license,\n restrictions, quotas, obligations, scopes and part_label are the\n publisher's, unchanged.\n\n Where a term is reachable only under an existing subscription, its offer\n carries `subscription_id` and prices at zero marginal cost; the terms a\n requester may see at all are selected by `LicenseTerm.scopes`.\n\n See: Universal Licensing Core section.").optional(), "title": z.string().describe("Resource title (human-readable, for display/logging).").optional() }).describe("Offer — A single resource offer from an Exchange.\n\nCombines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via fora-comp-v1 extension profile.")))(); export const OfferAbsenceReasonSchema = /* @__PURE__ */ (() => wire(z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"])))(); -export const OfferGroupSchema = /* @__PURE__ */ (() => wire(z.object({ "absence_reason": z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"]).describe("Why no offers are available for this URI.\n Set only when `offers` is empty, and not always then: a resource the\n requester's scopes leave with no presentable offer is answered with empty\n `offers` and this field unset (the existence-hiding rule, stated on\n Requester.scopes). Enables agents/Brokers to distinguish\n \"resource not in catalog\" from \"resource blocked for your use case\" without\n trial-and-error transactions. Analogous to OpenRTB nbr codes and\n Shutterstock per-item error metadata in batch responses.").optional(), "discovery_method": z.enum(["DISCOVERY_METHOD_EXCHANGE","DISCOVERY_METHOD_SEARCH","DISCOVERY_METHOD_RECOMMENDATION","DISCOVERY_METHOD_SYNDICATION"]).describe("How this URI was discovered by the Broker (v2 extension point).\n v1: always DISCOVERY_METHOD_EXCHANGE (Broker queried an Exchange).\n v2: may include DISCOVERY_METHOD_SEARCH (URI found via search engine like Exa),\n DISCOVERY_METHOD_RECOMMENDATION, etc. The Broker discovers URIs\n through any source, then routes through Exchange for pricing/transaction.\n The discovery method does not affect the transaction flow — it's metadata\n for the agent to understand how the resource was found.").optional(), "offers": z.array(z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"fora.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("ResourceAttestation — Signed envelope of claims from a trusted party.\n\nA provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"fora.attestation.claims_schema\"].")).describe("Signed attestations about the resource at this URI.\n Attestations provide cryptographic proof of\n resource properties from trusted parties (providers or verification vendors).\n\nThree verification levels determine what is independently verifiable:\n Level 0 (no attestations): Resource may carry identifiers (DOI, IPTC GUID)\n for identification, but nothing is cryptographically verifiable.\n Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): Provider signs own claims with Ed25519 key.\n Agent can independently verify content hash and token count.\n CDN delivery failure + content hash mismatch are auto-disputable.\n Level 2 (third-party attested): Independent verification vendor crawled\n the resource and attested to its properties. Agent trusts the attestation\n (does not re-verify hash). Token count discrepancy is auto-disputable\n when corroborated by CDN response size.\n\n Multiple attestations may be present (e.g., provider self-attestation\n plus a third-party verification). Agents choose which to trust.").optional(), "data_as_of": z.string().datetime({ offset: true }).describe("When the offered data was current. For dynamic resources\n (resource_mutability = DYNAMIC), this is the snapshot timestamp.\n Enables the Broker to evaluate freshness: \"this credit report\n reflects data as of March 18\" or \"this drug database was updated today.\"\n\nNot set for STATIC resources (content doesn't change) or LIVE\n resources (content doesn't exist yet).\n\n The Broker compares this against RequestConstraints.max_data_age\n to filter stale offers. Example: agent requests max_data_age = 7 days,\n Broker drops offers where now() - data_as_of > 7 days.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource will be delivered.").default(0), "exchange": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("REQUIRED. Bare host of the Exchange that issued this offer (e.g.\n \"exchange.example\" or \"exchange.example:8081\"), in the form \"Request\n recipient\" defines in the file header. This is the execute-routing target:\n the agent, or a relaying Broker, sends the ExecuteTransaction call for this\n offer to this Exchange, and a Broker relaying a mixed batch groups the items\n by this value. Because it is an ordinary Offer field it falls inside the\n signed bytes (see `signature` below — the signature covers every field\n except `signature` / `signature_algorithm`), so an intermediary cannot\n redirect the execute call to a different Exchange without invalidating the\n offer, and it is what retires the X-FORA-Exchange-Endpoint transport header.\n It is also the audience statement of an ExecuteTransaction, which is why\n TransactionRequest carries no top-level `exchange`: on receipt, an Exchange\n MUST reject the request unless EVERY item's offer.exchange names its own\n domain. Presence is enforced because an empty value is unroutable — a\n relaying Broker has nothing to group or dial on, and the swap-protection\n above is vacuous when the signed bytes carry no recipient at all."), "expires_at": z.string().datetime({ offset: true }).describe("When this offer expires (ISO 8601).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "iab_categories": z.array(z.string()).describe("IAB Content Taxonomy category codes.\n Enables agents to filter offers by topic (e.g., \"only finance resources\").\n Uses IAB Content Taxonomy 3.1 codes.").optional(), "identity": z.object({ "c2pa_manifest": z.string().describe("C2PA content credentials manifest URI.\n Points to a sidecar or embedded C2PA manifest for this resource.\n C2PA-aware agents MAY follow this URI to validate the full provenance\n chain (creator identity, transformation history, ingredient composition)\n using C2PA libraries (JUMBF/COSE Sign1). C2PA-unaware agents can rely\n on c2pa_status and c2pa-bridged attestation claims instead.\n\nFormats:\n Sidecar: HTTPS URI to a .c2pa manifest file\n Embedded: same URI as canonical_url (manifest is inside the asset)\n Content Credentials Cloud: https://contentcredentials.org/verify?uri=...").optional(), "c2pa_status": z.enum(["C2PA_STATUS_TRUSTED","C2PA_STATUS_VALID","C2PA_STATUS_INVALID","C2PA_STATUS_ABSENT"]).describe("The full C2PA validation details (signer identity, trust list,\n action history, training/mining status) are carried in a\n ResourceAttestation with c2pa.* claims — see fora-c2pa-v1 profile.").optional(), "canonical_url": z.string().describe("Provider's authoritative URL for this resource (rel=\"canonical\").\n Always available. Different per provider for syndicated content.").optional(), "content_hash": z.string().describe("Hash of the content. Interpretation depends on hash_method:\n \"simhash-v1\" → locality-sensitive hash, for fuzzy dedup (Level 1)\n \"sha256\" → exact-match integrity hash (Level 2)\n\nLevel 1 (SimHash): computed by Exchange from extracted text.\n Agent verifies that fetched content is \"substantially similar.\"\n Tolerates dynamic page elements.\n\n Level 2 (SHA-256): computed by provider from deterministic payload.\n Agent verifies exact match. Requires provider to serve consistent\n content (e.g., API endpoint, static HTML, structured JSON).\n Mismatch = dispute. Commands premium pricing.").optional(), "doi": z.string().describe("Digital Object Identifier — persistent, never changes.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().describe("Hash algorithm and verification level.\n Examples: \"simhash-v1\", \"minhash-v1\", \"sha256\", \"sha384\"").optional(), "iptc_guid": z.string().describe("IPTC NewsML-G2 globally unique identifier.\n Present when resource flows through news wire syndication (AP, Reuters).").optional(), "isni": z.string().describe("International Standard Name Identifier for the creator.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Drives hash verification behavior:\n STATIC: content_hash is stable. Agent SHOULD verify delivered content matches.\n DYNAMIC: content changes between offer and fetch (credit reports, drug databases).\n content_hash reflects state at offer generation time. Hash mismatch is\n expected and MUST NOT trigger automatic dispute.\n LIVE: content does not exist at offer time (streaming feeds, live broadcasts).\n content_hash is not applicable. The \"resource\" is the stream endpoint.\n\n Validated across 18 use cases: static content (articles, patents, legislation),\n dynamic data (credit reports, drug interactions, stock snapshots), and live\n streams (MarketData quotes, NPR broadcast, news monitoring feeds)."), "soft_binding": z.string().describe("Soft binding hash — content-derived identifier that survives format\n transcoding (resolution changes, compression, PDF-to-text extraction).\n Extracted from C2PA soft binding assertion when present.\n Enables post-delivery verification when the hard binding hash breaks\n due to legitimate format conversion.\n\nAlgorithm specified in soft_binding_method. Values are algorithm-specific\n (e.g., perceptual hash hex string, watermark identifier).").optional(), "soft_binding_method": z.string().describe("Algorithm used for soft_binding.\n Examples: \"phash-v1\" (perceptual hash), \"c2pa-watermark\" (C2PA invisible\n watermark), \"chromaprint\" (audio fingerprint).").optional() }).describe("Resource identity for cross-exchange deduplication.\n Enables Brokers to recognize the same resource offered by\n different Exchanges and compare pricing.").optional(), "offer_id": z.string().describe("Unique identifier for this offer, assigned by the Exchange.\n Opaque to the caller: not derived from the resource, its URL, or any\n other field, and carries no meaning beyond identifying this offer.\n Two offers for the same resource have different offer_ids.").default(""), "previews": z.array(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("Preview — Lightweight resource preview for offer evaluation.\n\nThe Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no FORA transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")).describe("Lightweight previews for offer evaluation.\n The Exchange holds URLs (50–200 bytes each); the provider's CDN serves\n the actual bytes. Agents fetch previews only when evaluating offers —\n not on every discovery query. Multiple previews at different sizes\n allow agents to pick the cheapest fetch for their evaluation needs.\n\nPer content type:\n Image: watermarked thumbnail (150–450px JPEG)\n Video: short clip (10–30s MP4, watermarked)\n Audio: short clip (15–30s MP3, low-bitrate or watermarked)\n Text: snippet or abstract (first 200 words as text/plain)\n Data: sample records (1–3 rows as application/json)\n Stream: optional frame capture or none (streams are priced by time)\n\n Modeled after Shutterstock (multi-size thumbnail URLs),\n Spotify (preview_url to 30s clip), IIIF (parameterized image URLs),\n and OpenRTB native (img.url + dimensions).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("The offer's price, and its ONLY price. An offer represents a single\n licensing arrangement: each projected LicenseTerm yields its own offer, and\n the Exchange moves that term's price here, so the term inside `terms`\n carries no pricing of its own (the offer.terms.pricing_unset rule). This is\n the price execute charges, the price a Broker ranks and compares across\n Exchanges, and the price a metered purchase is charged at. A resource with\n multiple alternative terms (e.g. dual-licensed) produces multiple separate\n offers, one per term — never one offer with a \"headline\" picked among them.\n\nIt derives from exactly one catalog term: the term's pricing as the\n publisher declared it (ResourceEntry.terms). Each offer derives from one\n term, and the offer carries no second copy, so the offer's price and its\n term's price cannot disagree.\n\n On a metered (PER_UNIT) offer, the rate and the estimate a metered\n purchase is charged at are read from here (see Pricing). The estimate is\n optional. When the publisher states one — on the term's own pricing, or\n once for the resource on ResourceEntry.estimated_quantity — the Exchange\n carries it here as estimated_quantity, the term's own estimate taking\n precedence. When the publisher states none, the offer may carry none, and\n the purchase then charges one unit.").optional(), "reporting": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds). A report that arrives after\n the window closed is refused with USAGE_REPORT_REJECTION_REASON_WINDOW_EXPIRED\n (Connect code failed_precondition).").optional() }).describe("Post-usage reporting requirements for this offer.").optional(), "signature": z.string().describe("REQUIRED. Hex-encoded detached Ed25519 signature over the canonical\n serialization of the ENTIRE Offer — every field, including `pricing`,\n `terms` (the full licensing payload), `expires_at`, and `exchange`. Only\n `signature` and `signature_algorithm` are excluded from the signed bytes.\n `expires_at` is signed so the offer's validity window is\n integrity-protected: a relaying Broker cannot extend (or shorten) the TTL\n of a signed offer to replay it outside the window the Exchange intended.\n\nCANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n UNKNOWN FIELDS. A canonicalizer either OMITS content it has no schema for or\n PRESERVES it, and the rule follows from which:\n\n - OMITTING (e.g. proto-JSON, which emits only schema-defined fields): such a\n canonicalizer CANNOT reproduce the signed bytes of a message carrying\n unknown fields — what it renders silently drops part of what the signer\n covered. It MUST refuse the message rather than emit the reduced bytes,\n and a verifier built on it MUST reject rather than verify over them. The\n refusal binds at EVERY depth: a nested message and each element of a\n repeated or map field carries its own unknown-field set.\n - PRESERVING (a canonicalizer that carries unrecognized members through):\n it reproduces the signed bytes faithfully, so there is nothing to refuse.\n\n Either way an APPENDED field cannot pass: an omitting canonicalizer refuses\n the message, and a preserving one renders the appended member into bytes the\n signer never covered, so the signature fails. Without the refusal the omitting\n case would fail OPEN — an intermediary could add unknown fields to an\n already-signed message and leave its signature verifying, smuggling\n unauthenticated content through a message the recipient treats as verified.\n\n Extensions therefore ride in `ext` / `ext_critical`, which are defined fields\n and inside the signed bytes — never as undeclared field numbers.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.").default(""), "signature_algorithm": z.string().describe("JOSE/JWA algorithm identifier (RFC 8037 §3.1). Always 'EdDSA' for\n Ed25519. Advisory only: this field is cleared before the canonical\n payload is signed, so it is not covered by the signature.").default(""), "subscription_id": z.string().describe("If set, this offer is available under an existing subscription/deal.\n No per-request billing — usage tracked against subscription quota.\n Pricing.rate = \"0\" for subscription offers (zero marginal cost).\n The Broker SHOULD prefer subscription offers when available.").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("SubscriptionQuotaInfo — Proactive quota signaling for subscription access.\n\nAnalogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Subscription quota state, when this offer is under a subscription.\n Enables the agent to see remaining quota before committing.\n Multiple entries when the subscription has independent quotas\n (e.g., access count + spend cap).").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Obligation — A post-use behavioral requirement attached to a LicenseTerm.\n\nExamples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).max(64).describe("Post-use behavioral requirements.\n At most 64, for the reason quotas carries.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. Where it is required depends on the message that\n holds the term, so the requirement is a rule of that message rather than of\n this field:\n - On a catalog term (ResourceEntry.terms) it is REQUIRED regardless of\n semantics — an agent cannot act on a priceless term, so absent Pricing\n is a validation error at ingest (resource_entry.terms.pricing_required).\n model = FREE must be stated explicitly (absent Pricing is not free). A\n REFERENCE_ONLY term states its price here too; its License governs the\n human-readable terms but does not replace the machine-readable price.\n - On the term an offer carries (Offer.terms) it MUST be unset\n (offer.terms.pricing_unset): the offer's price is Offer.pricing, stated\n once.").optional(), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The unit being capped — an open vocabulary axis.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quota — A usage cap the term declares.\n\nA quota states how much a licensee may consume under the term before the\n term expires or must be renegotiated. Like a restriction, it is a declared\n term the parties agree to. Quotas are NOT billing quantities — billing is in\n Pricing.\n\n The metric vocabulary is authored ONLY in the (fora.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).max(64).describe("Usage caps. The agent must not exceed any individual Quota.\n At most 64, the bound every per-message list in this contract carries when\n no rule walks it more than once. It bounds what one term may carry, not the\n work of checking one — a validator walks every element it is handed before\n the cap is reported, so the cost of checking is bounded at the transport.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to. Defined-only: the axis set is\n CLOSED, and a number outside it is refused rather than ignored. A custom\n axis is RESTRICTION_KIND_OTHER, whose meaning rides in permitted/prohibited,\n so a new number was never the extension mechanism — accepting one would\n admit a restriction no consumer can evaluate onto a term whose default is\n BINDING (see advisory below), which fails open on the axis a publisher most\n needs honoured. Closing the axis does NOT bound the cost of the one-per-kind\n rule below, and must not be read as doing so: a number this rule refuses is\n still distinct from every other, so that rule's all() finds no duplicate to\n stop on and walks the list in full anyway. Its cost is bounded by the size\n test the rule itself carries."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restriction — A single constraint on one licensing dimension.\n\nRestrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: they are declared terms the parties agree to when the\n agent accepts the offer. The AGENT self-selects the term whose restrictions it\n can honour and bears compliance. The Exchange never enforces a restriction:\n it is NOT an Exchange-side gate the requester must pass to see or buy a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and by the scopes in the requester's signed\n request — see LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (fora.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — FORA user/organization categories")).max(8).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.\n At most 8, and this list is the one of the three that does NOT carry the\n contract's usual 64: only one restriction per axis is valid, the axis enum\n is defined-only, so four is the longest conformant list and eight leaves\n room for an axis this version does not have. Like the caps on quotas and\n obligations, this one bounds the DOCUMENT — how many restrictions one term\n may carry — and not the work of checking it: a validator walks every element\n it is handed before any cardinality rule is reported, so an over-cap list is\n traversed in full on its way to being refused.\n\nWhat makes this list different is that one rule walks it against ITSELF. The\n one-per-kind rule below is quadratic, so it carries its own size test and\n stays silent above this cap; a conformance guard holds the two numbers equal,\n because a cap raised without the test would leave the lists in between\n unchecked for duplicate axes and accepted. The neighbouring disjointness rule\n on each element is quadratic only in that element's two token lists, both\n capped at 64, so its cost is bounded per restriction and linear across the\n list — it needs no such test.").optional(), "scopes": z.array(z.string()).max(64).describe("Scope-gating: the Exchange presents an offer for this term only when the\n scopes in the requester's signed request (Requester.scopes) cover ALL of\n these scopes (AND-semantics). Empty = public. A subscription term is\n Pricing{model:FREE} + scopes:[\"subscription:...\"].\n\nRequester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n also require a verified delegation that grants these scopes before it\n presents the term. To a requester that presents none, it answers as if the\n term did not exist, as existence hiding requires (see Requester.scopes). An\n offer the Exchange presented is honoured until it expires.\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("LicenseTerm — Universal licensing unit.\n\nOne LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation — with one move: on an offer the\n term's price is Offer.pricing, and the term itself carries none.\n\n Validation rules:\n - Pricing MUST be present on EVERY catalog term (ResourceEntry.terms),\n regardless of semantics — the resource_entry.terms.pricing_required\n rule. Absent Pricing → reject at ingest: an agent cannot act on a term\n with no price. This holds for REFERENCE_ONLY too — its License governs\n the human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - Pricing MUST be unset on the term an offer carries (Offer.terms) — the\n offer.terms.pricing_unset rule. The offer's price is Offer.pricing, and\n each offer derives from exactly one catalog term, so the price is stated\n once and cannot disagree with itself. The requirement is therefore a\n rule of the message that holds the term, not of LicenseTerm.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - A PER_UNIT term MAY state Pricing.estimated_quantity. It is not\n required, on the pushed term or on the offer it projects to — see\n Pricing.estimated_quantity.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).min(1).max(1).describe("The licensing term this offer sells, sourced from the publisher's\n ResourceEntry. EXACTLY ONE, and the bound is enforced rather than asked\n for: an offer IS a single licensing arrangement, so N terms on a resource\n project to N offers on that resource, each selling one of them — see\n `pricing` above. An offer with no term would be an offer projected from\n nothing, which is why the floor is one and not zero. The field is\n `repeated` because it shipped that way in v1 and renumbering a released\n field is not available to us, so the cardinality rides as a validation\n rule instead. `ResourceEntry.terms` is the plural side, bounded at 32: a\n resource carries many terms, an offer sells one of them.\n\nThe distinction is not cosmetic. `pricing` is what a Broker ranks on and\n what execute charges, so a term that is not on its own offer has no price\n a Broker can compare and no offer_id an agent can buy. Fusing several\n terms onto one offer makes every term but the first unsellable — a\n dual-licensed resource sells only under whichever arrangement the\n publisher happened to store first.\n\n The term here carries everything the publisher declared EXCEPT its price:\n its `pricing` MUST be unset (the offer.terms.pricing_unset rule), because\n the offer's price is `pricing` above. Its semantics, license,\n restrictions, quotas, obligations, scopes and part_label are the\n publisher's, unchanged.\n\n Where a term is reachable only under an existing subscription, its offer\n carries `subscription_id` and prices at zero marginal cost; the terms a\n requester may see at all are selected by `LicenseTerm.scopes`.\n\n See: Universal Licensing Core section.").optional(), "title": z.string().describe("Resource title (human-readable, for display/logging).").optional() }).describe("Offer — A single resource offer from an Exchange.\n\nCombines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via fora-comp-v1 extension profile.")).describe("Zero or more offers for this URI. Empty = resource not available.").optional(), "restriction_filters": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("When absence_reason = RESTRICTION_FILTERED, the restriction axes that drove\n the convenience pre-filter, in the same RestrictionKind vocabulary the terms\n use (e.g. [GEOGRAPHY] when the requester's stated geography matched no term).\n Advisory diagnostics, not an enforcement verdict.").optional(), "uri": z.string().describe("The URI this group of offers is for (echoed from ResourceQuery.uris).").default("") }).describe("OfferGroup — Offers for a single requested URI.\n Enables multi-URI batch queries where the caller needs to know\n which offers correspond to which requested resource.")))(); +export const OfferGroupSchema = /* @__PURE__ */ (() => wire(z.object({ "absence_reason": z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"]).describe("Why no offers are available for this URI.\n Set only when `offers` is empty, and not always then: a resource the\n requester's scopes leave with no presentable offer is answered with empty\n `offers` and this field unset (the existence-hiding rule, stated on\n Requester.scopes). Enables agents/Brokers to distinguish\n \"resource not in catalog\" from \"resource blocked for your use case\" without\n trial-and-error transactions. Analogous to OpenRTB nbr codes and\n Shutterstock per-item error metadata in batch responses.").optional(), "discovery_method": z.enum(["DISCOVERY_METHOD_EXCHANGE","DISCOVERY_METHOD_SEARCH","DISCOVERY_METHOD_RECOMMENDATION","DISCOVERY_METHOD_SYNDICATION"]).describe("How this URI was discovered by the Broker (v2 extension point).\n v1: always DISCOVERY_METHOD_EXCHANGE (Broker queried an Exchange).\n v2: may include DISCOVERY_METHOD_SEARCH (URI found via search engine like Exa),\n DISCOVERY_METHOD_RECOMMENDATION, etc. The Broker discovers URIs\n through any source, then routes through Exchange for pricing/transaction.\n The discovery method does not affect the transaction flow — it's metadata\n for the agent to understand how the resource was found.").optional(), "offers": z.array(z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"fora.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("ResourceAttestation — Signed envelope of claims from a trusted party.\n\nA provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"fora.attestation.claims_schema\"].")).describe("Signed attestations about the resource at this URI.\n Attestations provide cryptographic proof of\n resource properties from trusted parties (providers or verification vendors).\n\nThree verification levels determine what is independently verifiable:\n Level 0 (no attestations): Resource may carry identifiers (DOI, IPTC GUID)\n for identification, but nothing is cryptographically verifiable.\n Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): Provider signs own claims with Ed25519 key.\n Agent can independently verify content hash and token count.\n CDN delivery failure + content hash mismatch are auto-disputable.\n Level 2 (third-party attested): Independent verification vendor crawled\n the resource and attested to its properties. Agent trusts the attestation\n (does not re-verify hash). Token count discrepancy is auto-disputable\n when corroborated by CDN response size.\n\n Multiple attestations may be present (e.g., provider self-attestation\n plus a third-party verification). Agents choose which to trust.").optional(), "data_as_of": z.string().datetime({ offset: true }).describe("When the offered data was current. For dynamic resources\n (resource_mutability = DYNAMIC), this is the snapshot timestamp.\n Enables the Broker to evaluate freshness: \"this credit report\n reflects data as of March 18\" or \"this drug database was updated today.\"\n\nNot set for STATIC resources (content doesn't change) or LIVE\n resources (content doesn't exist yet).\n\n The Broker compares this against RequestConstraints.max_data_age\n to filter stale offers. Example: agent requests max_data_age = 7 days,\n Broker drops offers where now() - data_as_of > 7 days.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource will be delivered.").default(0), "exchange": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("REQUIRED. Bare host of the Exchange that issued this offer (e.g.\n \"exchange.example\" or \"exchange.example:8081\"), in the form \"Request\n recipient\" defines in the file header. This is the execute-routing target:\n the agent, or a relaying Broker, sends the ExecuteTransaction call for this\n offer to this Exchange, and a Broker relaying a mixed batch groups the items\n by this value. Because it is an ordinary Offer field it falls inside the\n signed bytes (see `signature` below — the signature covers every field\n except `signature` / `signature_algorithm`), so an intermediary cannot\n redirect the execute call to a different Exchange without invalidating the\n offer, and it is what retires the X-FORA-Exchange-Endpoint transport header.\n It is also the audience statement of an ExecuteTransaction, which is why\n TransactionRequest carries no top-level `exchange`: on receipt, an Exchange\n MUST reject the request unless EVERY item's offer.exchange names its own\n domain. Presence is enforced because an empty value is unroutable — a\n relaying Broker has nothing to group or dial on, and the swap-protection\n above is vacuous when the signed bytes carry no recipient at all."), "expires_at": z.string().datetime({ offset: true }).describe("When this offer expires (ISO 8601). An offer for a term gated behind a\n verified delegation MUST NOT expire later than that delegation chain's\n effective expiry, the earliest `exp` in the chain (see Delegation).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "iab_categories": z.array(z.string()).describe("IAB Content Taxonomy category codes.\n Enables agents to filter offers by topic (e.g., \"only finance resources\").\n Uses IAB Content Taxonomy 3.1 codes.").optional(), "identity": z.object({ "c2pa_manifest": z.string().describe("C2PA content credentials manifest URI.\n Points to a sidecar or embedded C2PA manifest for this resource.\n C2PA-aware agents MAY follow this URI to validate the full provenance\n chain (creator identity, transformation history, ingredient composition)\n using C2PA libraries (JUMBF/COSE Sign1). C2PA-unaware agents can rely\n on c2pa_status and c2pa-bridged attestation claims instead.\n\nFormats:\n Sidecar: HTTPS URI to a .c2pa manifest file\n Embedded: same URI as canonical_url (manifest is inside the asset)\n Content Credentials Cloud: https://contentcredentials.org/verify?uri=...").optional(), "c2pa_status": z.enum(["C2PA_STATUS_TRUSTED","C2PA_STATUS_VALID","C2PA_STATUS_INVALID","C2PA_STATUS_ABSENT"]).describe("The full C2PA validation details (signer identity, trust list,\n action history, training/mining status) are carried in a\n ResourceAttestation with c2pa.* claims — see fora-c2pa-v1 profile.").optional(), "canonical_url": z.string().describe("Provider's authoritative URL for this resource (rel=\"canonical\").\n Always available. Different per provider for syndicated content.").optional(), "content_hash": z.string().describe("Hash of the content. Interpretation depends on hash_method:\n \"simhash-v1\" → locality-sensitive hash, for fuzzy dedup (Level 1)\n \"sha256\" → exact-match integrity hash (Level 2)\n\nLevel 1 (SimHash): computed by Exchange from extracted text.\n Agent verifies that fetched content is \"substantially similar.\"\n Tolerates dynamic page elements.\n\n Level 2 (SHA-256): computed by provider from deterministic payload.\n Agent verifies exact match. Requires provider to serve consistent\n content (e.g., API endpoint, static HTML, structured JSON).\n Mismatch = dispute. Commands premium pricing.").optional(), "doi": z.string().describe("Digital Object Identifier — persistent, never changes.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().describe("Hash algorithm and verification level.\n Examples: \"simhash-v1\", \"minhash-v1\", \"sha256\", \"sha384\"").optional(), "iptc_guid": z.string().describe("IPTC NewsML-G2 globally unique identifier.\n Present when resource flows through news wire syndication (AP, Reuters).").optional(), "isni": z.string().describe("International Standard Name Identifier for the creator.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Drives hash verification behavior:\n STATIC: content_hash is stable. Agent SHOULD verify delivered content matches.\n DYNAMIC: content changes between offer and fetch (credit reports, drug databases).\n content_hash reflects state at offer generation time. Hash mismatch is\n expected and MUST NOT trigger automatic dispute.\n LIVE: content does not exist at offer time (streaming feeds, live broadcasts).\n content_hash is not applicable. The \"resource\" is the stream endpoint.\n\n Validated across 18 use cases: static content (articles, patents, legislation),\n dynamic data (credit reports, drug interactions, stock snapshots), and live\n streams (MarketData quotes, NPR broadcast, news monitoring feeds)."), "soft_binding": z.string().describe("Soft binding hash — content-derived identifier that survives format\n transcoding (resolution changes, compression, PDF-to-text extraction).\n Extracted from C2PA soft binding assertion when present.\n Enables post-delivery verification when the hard binding hash breaks\n due to legitimate format conversion.\n\nAlgorithm specified in soft_binding_method. Values are algorithm-specific\n (e.g., perceptual hash hex string, watermark identifier).").optional(), "soft_binding_method": z.string().describe("Algorithm used for soft_binding.\n Examples: \"phash-v1\" (perceptual hash), \"c2pa-watermark\" (C2PA invisible\n watermark), \"chromaprint\" (audio fingerprint).").optional() }).describe("Resource identity for cross-exchange deduplication.\n Enables Brokers to recognize the same resource offered by\n different Exchanges and compare pricing.").optional(), "offer_id": z.string().describe("Unique identifier for this offer, assigned by the Exchange.\n Opaque to the caller: not derived from the resource, its URL, or any\n other field, and carries no meaning beyond identifying this offer.\n Two offers for the same resource have different offer_ids.").default(""), "previews": z.array(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("Preview — Lightweight resource preview for offer evaluation.\n\nThe Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no FORA transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")).describe("Lightweight previews for offer evaluation.\n The Exchange holds URLs (50–200 bytes each); the provider's CDN serves\n the actual bytes. Agents fetch previews only when evaluating offers —\n not on every discovery query. Multiple previews at different sizes\n allow agents to pick the cheapest fetch for their evaluation needs.\n\nPer content type:\n Image: watermarked thumbnail (150–450px JPEG)\n Video: short clip (10–30s MP4, watermarked)\n Audio: short clip (15–30s MP3, low-bitrate or watermarked)\n Text: snippet or abstract (first 200 words as text/plain)\n Data: sample records (1–3 rows as application/json)\n Stream: optional frame capture or none (streams are priced by time)\n\n Modeled after Shutterstock (multi-size thumbnail URLs),\n Spotify (preview_url to 30s clip), IIIF (parameterized image URLs),\n and OpenRTB native (img.url + dimensions).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("The offer's price, and its ONLY price. An offer represents a single\n licensing arrangement: each projected LicenseTerm yields its own offer, and\n the Exchange moves that term's price here, so the term inside `terms`\n carries no pricing of its own (the offer.terms.pricing_unset rule). This is\n the price execute charges, the price a Broker ranks and compares across\n Exchanges, and the price a metered purchase is charged at. A resource with\n multiple alternative terms (e.g. dual-licensed) produces multiple separate\n offers, one per term — never one offer with a \"headline\" picked among them.\n\nIt derives from exactly one catalog term: the term's pricing as the\n publisher declared it (ResourceEntry.terms). Each offer derives from one\n term, and the offer carries no second copy, so the offer's price and its\n term's price cannot disagree.\n\n On a metered (PER_UNIT) offer, the rate and the estimate a metered\n purchase is charged at are read from here (see Pricing). The estimate is\n optional. When the publisher states one — on the term's own pricing, or\n once for the resource on ResourceEntry.estimated_quantity — the Exchange\n carries it here as estimated_quantity, the term's own estimate taking\n precedence. When the publisher states none, the offer may carry none, and\n the purchase then charges one unit.").optional(), "reporting": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds). A report that arrives after\n the window closed is refused with USAGE_REPORT_REJECTION_REASON_WINDOW_EXPIRED\n (Connect code failed_precondition).").optional() }).describe("Post-usage reporting requirements for this offer.").optional(), "signature": z.string().describe("REQUIRED. Hex-encoded detached Ed25519 signature over the canonical\n serialization of the ENTIRE Offer — every field, including `pricing`,\n `terms` (the full licensing payload), `expires_at`, and `exchange`. Only\n `signature` and `signature_algorithm` are excluded from the signed bytes.\n `expires_at` is signed so the offer's validity window is\n integrity-protected: a relaying Broker cannot extend (or shorten) the TTL\n of a signed offer to replay it outside the window the Exchange intended.\n\nCANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n UNKNOWN FIELDS. A canonicalizer either OMITS content it has no schema for or\n PRESERVES it, and the rule follows from which:\n\n - OMITTING (e.g. proto-JSON, which emits only schema-defined fields): such a\n canonicalizer CANNOT reproduce the signed bytes of a message carrying\n unknown fields — what it renders silently drops part of what the signer\n covered. It MUST refuse the message rather than emit the reduced bytes,\n and a verifier built on it MUST reject rather than verify over them. The\n refusal binds at EVERY depth: a nested message and each element of a\n repeated or map field carries its own unknown-field set.\n - PRESERVING (a canonicalizer that carries unrecognized members through):\n it reproduces the signed bytes faithfully, so there is nothing to refuse.\n\n Either way an APPENDED field cannot pass: an omitting canonicalizer refuses\n the message, and a preserving one renders the appended member into bytes the\n signer never covered, so the signature fails. Without the refusal the omitting\n case would fail OPEN — an intermediary could add unknown fields to an\n already-signed message and leave its signature verifying, smuggling\n unauthenticated content through a message the recipient treats as verified.\n\n Extensions therefore ride in `ext` / `ext_critical`, which are defined fields\n and inside the signed bytes — never as undeclared field numbers.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.").default(""), "signature_algorithm": z.string().describe("JOSE/JWA algorithm identifier (RFC 8037 §3.1). Always 'EdDSA' for\n Ed25519. Advisory only: this field is cleared before the canonical\n payload is signed, so it is not covered by the signature.").default(""), "subscription_id": z.string().describe("If set, this offer is available under an existing subscription/deal.\n No per-request billing — usage tracked against subscription quota.\n Pricing.rate = \"0\" for subscription offers (zero marginal cost).\n The Broker SHOULD prefer subscription offers when available.").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("SubscriptionQuotaInfo — Proactive quota signaling for subscription access.\n\nAnalogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Subscription quota state, when this offer is under a subscription.\n Enables the agent to see remaining quota before committing.\n Multiple entries when the subscription has independent quotas\n (e.g., access count + spend cap).").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Obligation — A post-use behavioral requirement attached to a LicenseTerm.\n\nExamples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).max(64).describe("Post-use behavioral requirements.\n At most 64, for the reason quotas carries.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. Where it is required depends on the message that\n holds the term, so the requirement is a rule of that message rather than of\n this field:\n - On a catalog term (ResourceEntry.terms) it is REQUIRED regardless of\n semantics — an agent cannot act on a priceless term, so absent Pricing\n is a validation error at ingest (resource_entry.terms.pricing_required).\n model = FREE must be stated explicitly (absent Pricing is not free). A\n REFERENCE_ONLY term states its price here too; its License governs the\n human-readable terms but does not replace the machine-readable price.\n - On the term an offer carries (Offer.terms) it MUST be unset\n (offer.terms.pricing_unset): the offer's price is Offer.pricing, stated\n once.").optional(), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The unit being capped — an open vocabulary axis.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quota — A usage cap the term declares.\n\nA quota states how much a licensee may consume under the term before the\n term expires or must be renegotiated. Like a restriction, it is a declared\n term the parties agree to. Quotas are NOT billing quantities — billing is in\n Pricing.\n\n The metric vocabulary is authored ONLY in the (fora.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).max(64).describe("Usage caps. The agent must not exceed any individual Quota.\n At most 64, the bound every per-message list in this contract carries when\n no rule walks it more than once. It bounds what one term may carry, not the\n work of checking one — a validator walks every element it is handed before\n the cap is reported, so the cost of checking is bounded at the transport.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to. Defined-only: the axis set is\n CLOSED, and a number outside it is refused rather than ignored. A custom\n axis is RESTRICTION_KIND_OTHER, whose meaning rides in permitted/prohibited,\n so a new number was never the extension mechanism — accepting one would\n admit a restriction no consumer can evaluate onto a term whose default is\n BINDING (see advisory below), which fails open on the axis a publisher most\n needs honoured. Closing the axis does NOT bound the cost of the one-per-kind\n rule below, and must not be read as doing so: a number this rule refuses is\n still distinct from every other, so that rule's all() finds no duplicate to\n stop on and walks the list in full anyway. Its cost is bounded by the size\n test the rule itself carries."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restriction — A single constraint on one licensing dimension.\n\nRestrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: they are declared terms the parties agree to when the\n agent accepts the offer. The AGENT self-selects the term whose restrictions it\n can honour and bears compliance. The Exchange never enforces a restriction:\n it is NOT an Exchange-side gate the requester must pass to see or buy a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and by the scopes in the requester's signed\n request — see LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (fora.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — FORA user/organization categories")).max(8).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.\n At most 8, and this list is the one of the three that does NOT carry the\n contract's usual 64: only one restriction per axis is valid, the axis enum\n is defined-only, so four is the longest conformant list and eight leaves\n room for an axis this version does not have. Like the caps on quotas and\n obligations, this one bounds the DOCUMENT — how many restrictions one term\n may carry — and not the work of checking it: a validator walks every element\n it is handed before any cardinality rule is reported, so an over-cap list is\n traversed in full on its way to being refused.\n\nWhat makes this list different is that one rule walks it against ITSELF. The\n one-per-kind rule below is quadratic, so it carries its own size test and\n stays silent above this cap; a conformance guard holds the two numbers equal,\n because a cap raised without the test would leave the lists in between\n unchecked for duplicate axes and accepted. The neighbouring disjointness rule\n on each element is quadratic only in that element's two token lists, both\n capped at 64, so its cost is bounded per restriction and linear across the\n list — it needs no such test.").optional(), "scopes": z.array(z.string()).max(64).describe("Scope-gating: the Exchange presents an offer for this term only when the\n scopes in the requester's signed request (Requester.scopes) cover ALL of\n these scopes (AND-semantics). Empty = public. A subscription term is\n Pricing{model:FREE} + scopes:[\"subscription:...\"].\n\nRequester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n instead gate a term behind a verified delegation (a subscription term is\n the usual case): then only the delegation chain's verified effective scopes\n are matched against these scopes, and Requester.scopes is ignored for that\n term. To a requester that presents no valid delegation, it answers as if\n the term did not exist, as existence hiding requires (see\n Requester.scopes). An offer the Exchange presented is honoured until it\n expires, and a gated offer expires no later than the chain's effective\n expiry (see Delegation).\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments (at least one), and there is NO implicit prefix match\n (a grant narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("LicenseTerm — Universal licensing unit.\n\nOne LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation — with one move: on an offer the\n term's price is Offer.pricing, and the term itself carries none.\n\n Validation rules:\n - Pricing MUST be present on EVERY catalog term (ResourceEntry.terms),\n regardless of semantics — the resource_entry.terms.pricing_required\n rule. Absent Pricing → reject at ingest: an agent cannot act on a term\n with no price. This holds for REFERENCE_ONLY too — its License governs\n the human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - Pricing MUST be unset on the term an offer carries (Offer.terms) — the\n offer.terms.pricing_unset rule. The offer's price is Offer.pricing, and\n each offer derives from exactly one catalog term, so the price is stated\n once and cannot disagree with itself. The requirement is therefore a\n rule of the message that holds the term, not of LicenseTerm.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - A PER_UNIT term MAY state Pricing.estimated_quantity. It is not\n required, on the pushed term or on the offer it projects to — see\n Pricing.estimated_quantity.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).min(1).max(1).describe("The licensing term this offer sells, sourced from the publisher's\n ResourceEntry. EXACTLY ONE, and the bound is enforced rather than asked\n for: an offer IS a single licensing arrangement, so N terms on a resource\n project to N offers on that resource, each selling one of them — see\n `pricing` above. An offer with no term would be an offer projected from\n nothing, which is why the floor is one and not zero. The field is\n `repeated` because it shipped that way in v1 and renumbering a released\n field is not available to us, so the cardinality rides as a validation\n rule instead. `ResourceEntry.terms` is the plural side, bounded at 32: a\n resource carries many terms, an offer sells one of them.\n\nThe distinction is not cosmetic. `pricing` is what a Broker ranks on and\n what execute charges, so a term that is not on its own offer has no price\n a Broker can compare and no offer_id an agent can buy. Fusing several\n terms onto one offer makes every term but the first unsellable — a\n dual-licensed resource sells only under whichever arrangement the\n publisher happened to store first.\n\n The term here carries everything the publisher declared EXCEPT its price:\n its `pricing` MUST be unset (the offer.terms.pricing_unset rule), because\n the offer's price is `pricing` above. Its semantics, license,\n restrictions, quotas, obligations, scopes and part_label are the\n publisher's, unchanged.\n\n Where a term is reachable only under an existing subscription, its offer\n carries `subscription_id` and prices at zero marginal cost; the terms a\n requester may see at all are selected by `LicenseTerm.scopes`.\n\n See: Universal Licensing Core section.").optional(), "title": z.string().describe("Resource title (human-readable, for display/logging).").optional() }).describe("Offer — A single resource offer from an Exchange.\n\nCombines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via fora-comp-v1 extension profile.")).describe("Zero or more offers for this URI. Empty = resource not available.").optional(), "restriction_filters": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("When absence_reason = RESTRICTION_FILTERED, the restriction axes that drove\n the convenience pre-filter, in the same RestrictionKind vocabulary the terms\n use (e.g. [GEOGRAPHY] when the requester's stated geography matched no term).\n Advisory diagnostics, not an enforcement verdict.").optional(), "uri": z.string().describe("The URI this group of offers is for (echoed from ResourceQuery.uris).").default("") }).describe("OfferGroup — Offers for a single requested URI.\n Enables multi-URI batch queries where the caller needs to know\n which offers correspond to which requested resource.")))(); export const PreviewSchema = /* @__PURE__ */ (() => wire(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("Preview — Lightweight resource preview for offer evaluation.\n\nThe Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no FORA transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")))(); @@ -116,7 +116,7 @@ export const PricingModelSchema = /* @__PURE__ */ (() => wire(z.enum(["PRICING_M export const ProviderRelationshipSchema = /* @__PURE__ */ (() => wire(z.enum(["PROVIDER_RELATIONSHIP_DIRECT","PROVIDER_RELATIONSHIP_RESELLER"])))(); -export const PushResourcesRequestSchema = /* @__PURE__ */ (() => wire(z.object({ "caller_id": z.string().describe("DEPRECATED, never needed. Each entry's domain names whose resource it is,\n and the caller is the party that signs the request: the Exchange checks\n that the verified signer may push for each entry's domain (see\n CATALOG_REJECTION_REASON_NOT_CATALOG_CONTRIBUTOR), and a receiver does not\n rely on this field. The field is retained because removing it would break\n the v1 wire contract. It does not yet carry the `deprecated` field option,\n so implementations that still read it keep building; the option follows\n once they no longer do.").default(""), "entries": z.array(z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"fora.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("ResourceAttestation — Signed envelope of claims from a trusted party.\n\nA provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"fora.attestation.claims_schema\"].")).max(64).describe("Signed attestations about this resource entry.\n Same semantics as Offer.attestations — see ResourceAttestation message\n for verification levels and claim vocabulary. Attestations pushed via\n CatalogService are verified at push time: the Exchange checks that\n the attestation verifier is authorized to push for this provider\n (via catalog_contributors in the provider's WellKnownManifest) and validates the\n attestation signature against the verifier's public key from its WBA\n directory (the JWK Set at /.well-known/http-message-signatures-directory;\n the keyid is the key's RFC 7638 thumbprint). The verifier's fora.json\n carries only its role, determined by the verifier's operator.").optional(), "content_hash": z.string().max(255).describe("Content hash, carried as the publisher computed it — a bare hex digest or a\n \"method:hexdigest\" form; bounded in length, never format-checked, because\n hash_method names the algorithm.").optional(), "content_id": z.string().max(255).describe("Content identifier").optional(), "domain": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("Provider domain — the bare host the resource lives on, in the shape\n \"Request recipient\" defines in the file header: a port is allowed, a\n scheme, path, query or userinfo is not. With path it forms the catalog URI\n by concatenation, so a value carrying anything but a host would choose the\n URI rather than merely name the host."), "estimated_quantity": z.coerce.number().int().gte(0).lt(2147483648).describe("Estimated quantity in the metering unit").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052): keys within ext\n that the AGENT must understand. The Exchange carries them onto\n Offer.ext_critical and does not consume them, so an unknown key here never\n refuses the push (see \"Critical extensions\" in the file header). Empty\n (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().max(64).describe("Hash algorithm").optional(), "path": z.string().regex(new RegExp("^/[^?#\\x00-\\x20\\x7f]*$")).min(1).max(2048).describe("Content path — an absolute URL path such as \"/premium/article-42.html\":\n starts with \"/\", carries no query or fragment delimiter, no whitespace and\n no control character, and is at most 2048 characters. Characters, not bytes:\n protovalidate's max_len counts Unicode code points, and the pattern admits\n non-ASCII, so a conformant path can exceed 2048 bytes."), "provenance_source": z.string().max(260).describe("Who provided this resource metadata. Creates audit trail for\n \"where did this catalog entry come from?\"").optional(), "provenance_timestamp": z.string().datetime({ offset: true }).describe("When this metadata was collected/generated.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Optional mutability hint. When omitted, the Exchange applies the `STATIC`\n default at Offer build; an explicit `UNSPECIFIED` is rejected. A value in\n `ext` is not read — the typed field is authoritative, so an ext-only value\n is treated as omitted. Mirrors the required Offer-side\n `ResourceIdentity.resource_mutability`.").optional(), "source": z.enum(["INGESTION_SOURCE_FORA_SITEMAP","INGESTION_SOURCE_RSL","INGESTION_SOURCE_SITEMAP","INGESTION_SOURCE_HTML_CRAWL","INGESTION_SOURCE_CMS_API","INGESTION_SOURCE_MANUAL","INGESTION_SOURCE_CATALOG_API"]).describe("How the entry was discovered").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Obligation — A post-use behavioral requirement attached to a LicenseTerm.\n\nExamples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).max(64).describe("Post-use behavioral requirements.\n At most 64, for the reason quotas carries.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. Where it is required depends on the message that\n holds the term, so the requirement is a rule of that message rather than of\n this field:\n - On a catalog term (ResourceEntry.terms) it is REQUIRED regardless of\n semantics — an agent cannot act on a priceless term, so absent Pricing\n is a validation error at ingest (resource_entry.terms.pricing_required).\n model = FREE must be stated explicitly (absent Pricing is not free). A\n REFERENCE_ONLY term states its price here too; its License governs the\n human-readable terms but does not replace the machine-readable price.\n - On the term an offer carries (Offer.terms) it MUST be unset\n (offer.terms.pricing_unset): the offer's price is Offer.pricing, stated\n once.").optional(), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The unit being capped — an open vocabulary axis.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quota — A usage cap the term declares.\n\nA quota states how much a licensee may consume under the term before the\n term expires or must be renegotiated. Like a restriction, it is a declared\n term the parties agree to. Quotas are NOT billing quantities — billing is in\n Pricing.\n\n The metric vocabulary is authored ONLY in the (fora.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).max(64).describe("Usage caps. The agent must not exceed any individual Quota.\n At most 64, the bound every per-message list in this contract carries when\n no rule walks it more than once. It bounds what one term may carry, not the\n work of checking one — a validator walks every element it is handed before\n the cap is reported, so the cost of checking is bounded at the transport.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to. Defined-only: the axis set is\n CLOSED, and a number outside it is refused rather than ignored. A custom\n axis is RESTRICTION_KIND_OTHER, whose meaning rides in permitted/prohibited,\n so a new number was never the extension mechanism — accepting one would\n admit a restriction no consumer can evaluate onto a term whose default is\n BINDING (see advisory below), which fails open on the axis a publisher most\n needs honoured. Closing the axis does NOT bound the cost of the one-per-kind\n rule below, and must not be read as doing so: a number this rule refuses is\n still distinct from every other, so that rule's all() finds no duplicate to\n stop on and walks the list in full anyway. Its cost is bounded by the size\n test the rule itself carries."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restriction — A single constraint on one licensing dimension.\n\nRestrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: they are declared terms the parties agree to when the\n agent accepts the offer. The AGENT self-selects the term whose restrictions it\n can honour and bears compliance. The Exchange never enforces a restriction:\n it is NOT an Exchange-side gate the requester must pass to see or buy a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and by the scopes in the requester's signed\n request — see LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (fora.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — FORA user/organization categories")).max(8).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.\n At most 8, and this list is the one of the three that does NOT carry the\n contract's usual 64: only one restriction per axis is valid, the axis enum\n is defined-only, so four is the longest conformant list and eight leaves\n room for an axis this version does not have. Like the caps on quotas and\n obligations, this one bounds the DOCUMENT — how many restrictions one term\n may carry — and not the work of checking it: a validator walks every element\n it is handed before any cardinality rule is reported, so an over-cap list is\n traversed in full on its way to being refused.\n\nWhat makes this list different is that one rule walks it against ITSELF. The\n one-per-kind rule below is quadratic, so it carries its own size test and\n stays silent above this cap; a conformance guard holds the two numbers equal,\n because a cap raised without the test would leave the lists in between\n unchecked for duplicate axes and accepted. The neighbouring disjointness rule\n on each element is quadratic only in that element's two token lists, both\n capped at 64, so its cost is bounded per restriction and linear across the\n list — it needs no such test.").optional(), "scopes": z.array(z.string()).max(64).describe("Scope-gating: the Exchange presents an offer for this term only when the\n scopes in the requester's signed request (Requester.scopes) cover ALL of\n these scopes (AND-semantics). Empty = public. A subscription term is\n Pricing{model:FREE} + scopes:[\"subscription:...\"].\n\nRequester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n also require a verified delegation that grants these scopes before it\n presents the term. To a requester that presents none, it answers as if the\n term did not exist, as existence hiding requires (see Requester.scopes). An\n offer the Exchange presented is honoured until it expires.\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("LicenseTerm — Universal licensing unit.\n\nOne LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation — with one move: on an offer the\n term's price is Offer.pricing, and the term itself carries none.\n\n Validation rules:\n - Pricing MUST be present on EVERY catalog term (ResourceEntry.terms),\n regardless of semantics — the resource_entry.terms.pricing_required\n rule. Absent Pricing → reject at ingest: an agent cannot act on a term\n with no price. This holds for REFERENCE_ONLY too — its License governs\n the human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - Pricing MUST be unset on the term an offer carries (Offer.terms) — the\n offer.terms.pricing_unset rule. The offer's price is Offer.pricing, and\n each offer derives from exactly one catalog term, so the price is stated\n once and cannot disagree with itself. The requirement is therefore a\n rule of the message that holds the term, not of LicenseTerm.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - A PER_UNIT term MAY state Pricing.estimated_quantity. It is not\n required, on the pushed term or on the offer it projects to — see\n Pricing.estimated_quantity.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).max(32).describe("Publisher-declared licensing terms for this resource.\n See LicenseTerm for the full model. Every term carries its Pricing (the\n resource_entry.terms.pricing_required rule above), whatever its semantics.\n For REFERENCE_ONLY terms, License.uri is authoritative.\n The Exchange validates ENUMERATED terms at push time and surfaces each term\n on its own offer on discovery: the term's price becomes Offer.pricing, and\n the term in Offer.terms carries no pricing. At most 32 terms per entry,\n stated on the wire so every implementation refuses the same size. An over-cap entry refuses the\n whole submission, as every catalog rejection does; what being a wire rule\n changes is WHEN — the refusal now happens at the boundary, before any\n per-entry classification runs, which is why the rejection reason that named\n this cap can no longer be produced for a push.").optional(), "title": z.string().max(512).describe("Content title").optional(), "word_count": z.coerce.number().int().gte(0).lt(2147483648).describe("Word count").optional() }).describe("ResourceEntry — one catalog row as a publisher (or an authorised contributor)\n pushes it. The envelope fields carry their own wire rules, so an entry that\n cannot become a catalog URI is refused at the boundary rather than after\n ingestion; the licensing terms inside carry the LicenseTerm rules. See\n CatalogService for the second, ingest-time tier (token canonicalisation and\n registry membership).\n\nThe list caps here and on LicenseTerm bound HOW MANY, never how large: how many\n terms one entry may carry, how many entries a push can store, and how many paths\n a rejection has to name back. They do not bound the entry's SIZE either — several\n members inside one carry no length rule — and they do NOT bound the work of\n checking a push, and must not be read as doing so — a\n validator walks every element it is handed and reports every violation before\n any cardinality rule is applied, so an over-cap list is fully traversed on its\n way to being refused. The work is bounded one layer down, by the maximum\n request size the recipient will read; a deployment that exposes CatalogService\n sets that cap, and the SDK's server binding sets a default.")).min(1).max(256).describe("Content entries to push. At least one: an empty push asks for nothing and\n is refused rather than answered with zero counts. At most 256, the bound a\n caller-chosen batch carries elsewhere in this contract (see ResourceQuery.uris)\n — it bounds one submission, so a larger feed is pushed in several. The cap is\n over entries because a submission is stored or refused whole, and a refusal\n names each entry that failed; it does not bound the work of checking a\n submission, which the recipient bounds at the transport.\n\nBoth bounds are wire rules (min_items and max_items below). A push outside\n them is a malformed request, refused by wire validation with the Connect\n code invalid_argument like any other malformed request; it carries no\n catalog_rejection, because no entry was examined.").optional(), "exchange": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("REQUIRED. Bare host of the recipient this request is addressed to (e.g.\n \"exchange.example\" or \"exchange.example:8081\"). See \"Request recipient\" in\n the file header. Distinct from `tenant_id` above, which names a publisher\n tenant WITHIN an Exchange, not the Exchange itself."), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "tenant_id": z.string().describe("Tenant identifier").default(""), "ver": z.string().describe("FORA protocol version — \"1.0\". Stamped by the sender from a single\n constant; advisory on receive. See \"Protocol version\" in the file header.").default("") })))(); +export const PushResourcesRequestSchema = /* @__PURE__ */ (() => wire(z.object({ "caller_id": z.string().describe("DEPRECATED, never needed. Each entry's domain names whose resource it is,\n and the caller is the party that signs the request: the Exchange checks\n that the verified signer may push for each entry's domain (see\n CATALOG_REJECTION_REASON_NOT_CATALOG_CONTRIBUTOR), and a receiver does not\n rely on this field. The field is retained because removing it would break\n the v1 wire contract. It does not yet carry the `deprecated` field option,\n so implementations that still read it keep building; the option follows\n once they no longer do.").default(""), "entries": z.array(z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"fora.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("ResourceAttestation — Signed envelope of claims from a trusted party.\n\nA provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"fora.attestation.claims_schema\"].")).max(64).describe("Signed attestations about this resource entry.\n Same semantics as Offer.attestations — see ResourceAttestation message\n for verification levels and claim vocabulary. Attestations pushed via\n CatalogService are verified at push time: the Exchange checks that\n the attestation verifier is authorized to push for this provider\n (via catalog_contributors in the provider's WellKnownManifest) and validates the\n attestation signature against the verifier's public key from its WBA\n directory (the JWK Set at /.well-known/http-message-signatures-directory;\n the keyid is the key's RFC 7638 thumbprint). The verifier's fora.json\n carries only its role, determined by the verifier's operator.").optional(), "content_hash": z.string().max(255).describe("Content hash, carried as the publisher computed it — a bare hex digest or a\n \"method:hexdigest\" form; bounded in length, never format-checked, because\n hash_method names the algorithm.").optional(), "content_id": z.string().max(255).describe("Content identifier").optional(), "domain": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("Provider domain — the bare host the resource lives on, in the shape\n \"Request recipient\" defines in the file header: a port is allowed, a\n scheme, path, query or userinfo is not. With path it forms the catalog URI\n by concatenation, so a value carrying anything but a host would choose the\n URI rather than merely name the host."), "estimated_quantity": z.coerce.number().int().gte(0).lt(2147483648).describe("Estimated quantity in the metering unit").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052): keys within ext\n that the AGENT must understand. The Exchange carries them onto\n Offer.ext_critical and does not consume them, so an unknown key here never\n refuses the push (see \"Critical extensions\" in the file header). Empty\n (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().max(64).describe("Hash algorithm").optional(), "path": z.string().regex(new RegExp("^/[^?#\\x00-\\x20\\x7f]*$")).min(1).max(2048).describe("Content path — an absolute URL path such as \"/premium/article-42.html\":\n starts with \"/\", carries no query or fragment delimiter, no whitespace and\n no control character, and is at most 2048 characters. Characters, not bytes:\n protovalidate's max_len counts Unicode code points, and the pattern admits\n non-ASCII, so a conformant path can exceed 2048 bytes."), "provenance_source": z.string().max(260).describe("Who provided this resource metadata. Creates audit trail for\n \"where did this catalog entry come from?\"").optional(), "provenance_timestamp": z.string().datetime({ offset: true }).describe("When this metadata was collected/generated.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Optional mutability hint. When omitted, the Exchange applies the `STATIC`\n default at Offer build; an explicit `UNSPECIFIED` is rejected. A value in\n `ext` is not read — the typed field is authoritative, so an ext-only value\n is treated as omitted. Mirrors the required Offer-side\n `ResourceIdentity.resource_mutability`.").optional(), "source": z.enum(["INGESTION_SOURCE_FORA_SITEMAP","INGESTION_SOURCE_RSL","INGESTION_SOURCE_SITEMAP","INGESTION_SOURCE_HTML_CRAWL","INGESTION_SOURCE_CMS_API","INGESTION_SOURCE_MANUAL","INGESTION_SOURCE_CATALOG_API"]).describe("How the entry was discovered").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Obligation — A post-use behavioral requirement attached to a LicenseTerm.\n\nExamples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).max(64).describe("Post-use behavioral requirements.\n At most 64, for the reason quotas carries.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. Where it is required depends on the message that\n holds the term, so the requirement is a rule of that message rather than of\n this field:\n - On a catalog term (ResourceEntry.terms) it is REQUIRED regardless of\n semantics — an agent cannot act on a priceless term, so absent Pricing\n is a validation error at ingest (resource_entry.terms.pricing_required).\n model = FREE must be stated explicitly (absent Pricing is not free). A\n REFERENCE_ONLY term states its price here too; its License governs the\n human-readable terms but does not replace the machine-readable price.\n - On the term an offer carries (Offer.terms) it MUST be unset\n (offer.terms.pricing_unset): the offer's price is Offer.pricing, stated\n once.").optional(), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The unit being capped — an open vocabulary axis.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quota — A usage cap the term declares.\n\nA quota states how much a licensee may consume under the term before the\n term expires or must be renegotiated. Like a restriction, it is a declared\n term the parties agree to. Quotas are NOT billing quantities — billing is in\n Pricing.\n\n The metric vocabulary is authored ONLY in the (fora.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).max(64).describe("Usage caps. The agent must not exceed any individual Quota.\n At most 64, the bound every per-message list in this contract carries when\n no rule walks it more than once. It bounds what one term may carry, not the\n work of checking one — a validator walks every element it is handed before\n the cap is reported, so the cost of checking is bounded at the transport.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to. Defined-only: the axis set is\n CLOSED, and a number outside it is refused rather than ignored. A custom\n axis is RESTRICTION_KIND_OTHER, whose meaning rides in permitted/prohibited,\n so a new number was never the extension mechanism — accepting one would\n admit a restriction no consumer can evaluate onto a term whose default is\n BINDING (see advisory below), which fails open on the axis a publisher most\n needs honoured. Closing the axis does NOT bound the cost of the one-per-kind\n rule below, and must not be read as doing so: a number this rule refuses is\n still distinct from every other, so that rule's all() finds no duplicate to\n stop on and walks the list in full anyway. Its cost is bounded by the size\n test the rule itself carries."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restriction — A single constraint on one licensing dimension.\n\nRestrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: they are declared terms the parties agree to when the\n agent accepts the offer. The AGENT self-selects the term whose restrictions it\n can honour and bears compliance. The Exchange never enforces a restriction:\n it is NOT an Exchange-side gate the requester must pass to see or buy a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and by the scopes in the requester's signed\n request — see LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (fora.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — FORA user/organization categories")).max(8).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.\n At most 8, and this list is the one of the three that does NOT carry the\n contract's usual 64: only one restriction per axis is valid, the axis enum\n is defined-only, so four is the longest conformant list and eight leaves\n room for an axis this version does not have. Like the caps on quotas and\n obligations, this one bounds the DOCUMENT — how many restrictions one term\n may carry — and not the work of checking it: a validator walks every element\n it is handed before any cardinality rule is reported, so an over-cap list is\n traversed in full on its way to being refused.\n\nWhat makes this list different is that one rule walks it against ITSELF. The\n one-per-kind rule below is quadratic, so it carries its own size test and\n stays silent above this cap; a conformance guard holds the two numbers equal,\n because a cap raised without the test would leave the lists in between\n unchecked for duplicate axes and accepted. The neighbouring disjointness rule\n on each element is quadratic only in that element's two token lists, both\n capped at 64, so its cost is bounded per restriction and linear across the\n list — it needs no such test.").optional(), "scopes": z.array(z.string()).max(64).describe("Scope-gating: the Exchange presents an offer for this term only when the\n scopes in the requester's signed request (Requester.scopes) cover ALL of\n these scopes (AND-semantics). Empty = public. A subscription term is\n Pricing{model:FREE} + scopes:[\"subscription:...\"].\n\nRequester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n instead gate a term behind a verified delegation (a subscription term is\n the usual case): then only the delegation chain's verified effective scopes\n are matched against these scopes, and Requester.scopes is ignored for that\n term. To a requester that presents no valid delegation, it answers as if\n the term did not exist, as existence hiding requires (see\n Requester.scopes). An offer the Exchange presented is honoured until it\n expires, and a gated offer expires no later than the chain's effective\n expiry (see Delegation).\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments (at least one), and there is NO implicit prefix match\n (a grant narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("LicenseTerm — Universal licensing unit.\n\nOne LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation — with one move: on an offer the\n term's price is Offer.pricing, and the term itself carries none.\n\n Validation rules:\n - Pricing MUST be present on EVERY catalog term (ResourceEntry.terms),\n regardless of semantics — the resource_entry.terms.pricing_required\n rule. Absent Pricing → reject at ingest: an agent cannot act on a term\n with no price. This holds for REFERENCE_ONLY too — its License governs\n the human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - Pricing MUST be unset on the term an offer carries (Offer.terms) — the\n offer.terms.pricing_unset rule. The offer's price is Offer.pricing, and\n each offer derives from exactly one catalog term, so the price is stated\n once and cannot disagree with itself. The requirement is therefore a\n rule of the message that holds the term, not of LicenseTerm.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - A PER_UNIT term MAY state Pricing.estimated_quantity. It is not\n required, on the pushed term or on the offer it projects to — see\n Pricing.estimated_quantity.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).max(32).describe("Publisher-declared licensing terms for this resource.\n See LicenseTerm for the full model. Every term carries its Pricing (the\n resource_entry.terms.pricing_required rule above), whatever its semantics.\n For REFERENCE_ONLY terms, License.uri is authoritative.\n The Exchange validates ENUMERATED terms at push time and surfaces each term\n on its own offer on discovery: the term's price becomes Offer.pricing, and\n the term in Offer.terms carries no pricing. At most 32 terms per entry,\n stated on the wire so every implementation refuses the same size. An over-cap entry refuses the\n whole submission, as every catalog rejection does; what being a wire rule\n changes is WHEN — the refusal now happens at the boundary, before any\n per-entry classification runs, which is why the rejection reason that named\n this cap can no longer be produced for a push.").optional(), "title": z.string().max(512).describe("Content title").optional(), "word_count": z.coerce.number().int().gte(0).lt(2147483648).describe("Word count").optional() }).describe("ResourceEntry — one catalog row as a publisher (or an authorised contributor)\n pushes it. The envelope fields carry their own wire rules, so an entry that\n cannot become a catalog URI is refused at the boundary rather than after\n ingestion; the licensing terms inside carry the LicenseTerm rules. See\n CatalogService for the second, ingest-time tier (token canonicalisation and\n registry membership).\n\nThe list caps here and on LicenseTerm bound HOW MANY, never how large: how many\n terms one entry may carry, how many entries a push can store, and how many paths\n a rejection has to name back. They do not bound the entry's SIZE either — several\n members inside one carry no length rule — and they do NOT bound the work of\n checking a push, and must not be read as doing so — a\n validator walks every element it is handed and reports every violation before\n any cardinality rule is applied, so an over-cap list is fully traversed on its\n way to being refused. The work is bounded one layer down, by the maximum\n request size the recipient will read; a deployment that exposes CatalogService\n sets that cap, and the SDK's server binding sets a default.")).min(1).max(256).describe("Content entries to push. At least one: an empty push asks for nothing and\n is refused rather than answered with zero counts. At most 256, the bound a\n caller-chosen batch carries elsewhere in this contract (see ResourceQuery.uris)\n — it bounds one submission, so a larger feed is pushed in several. The cap is\n over entries because a submission is stored or refused whole, and a refusal\n names each entry that failed; it does not bound the work of checking a\n submission, which the recipient bounds at the transport.\n\nBoth bounds are wire rules (min_items and max_items below). A push outside\n them is a malformed request, refused by wire validation with the Connect\n code invalid_argument like any other malformed request; it carries no\n catalog_rejection, because no entry was examined.").optional(), "exchange": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("REQUIRED. Bare host of the recipient this request is addressed to (e.g.\n \"exchange.example\" or \"exchange.example:8081\"). See \"Request recipient\" in\n the file header. Distinct from `tenant_id` above, which names a publisher\n tenant WITHIN an Exchange, not the Exchange itself."), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "tenant_id": z.string().describe("Tenant identifier").default(""), "ver": z.string().describe("FORA protocol version — \"1.0\". Stamped by the sender from a single\n constant; advisory on receive. See \"Protocol version\" in the file header.").default("") })))(); export const PushResourcesResponseSchema = /* @__PURE__ */ (() => wire(z.object({ "accepted": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Number of entries accepted. A push is all-or-nothing, so a successful push\n stored every entry it carried and this is the submission's own size. A push\n the Exchange processed and could not apply is not a response at all: it\n travels as a non-OK transport error carrying ErrorDetail.catalog_rejection.\n A push refused before processing is answered by the general rules instead:\n a mis-addressed push (see \"Request recipient\" in the file header) and a\n malformed one are invalid_argument with no typed reason, and one whose\n signature fails is unauthenticated with ErrorDetail.request_auth_failure.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "rejected": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Number of entries rejected. Structurally always 0 on this path, and kept for\n the same reason CATALOG_REJECTION_REASON_TERMS_LIMIT_EXCEEDED is kept: a\n rejection returns an error rather than a response, so there is no successful\n answer in which this can be non-zero. It remains meaningful only for a\n deployment that applies catalog rules somewhere the all-or-nothing rule above\n does not front. Do NOT read a zero here as \"nothing failed\" — read `accepted`.").optional(), "ver": z.string().describe("FORA protocol version — \"1.0\". Stamped by the sender from a single\n constant; advisory on receive. See \"Protocol version\" in the file header.").default(""), "warnings": z.array(z.string()).describe("Non-fatal issues encountered during ingestion — the ingest tier's lint, which\n accepts the term and flags it. Examples: an unregistered bare restriction\n token on any axis, and an OBLIGATION_KIND_OTHER obligation with no detail.\n Warnings do not cause rejection; they are surfaced so publishers can fix their\n feeds without a hard failure. A condition that rejects is not a warning — a\n REFERENCE_ONLY term with no License.uri, for instance, is refused by\n license_term.reference_only.requires_uri and never reaches this list.").optional() })))(); @@ -154,7 +154,7 @@ export const RequestAuthFailureReasonSchema = /* @__PURE__ */ (() => wire(z.enum export const RequestConstraintsSchema = /* @__PURE__ */ (() => wire(z.object({ "budget_period": z.string().describe("Budget period (e.g. \"2592000s\" = 30 days; proto-JSON encodes Duration\n as seconds).").optional(), "budget_scope": z.string().describe("Budget scope identifier: whose budget this is.\n E.g. \"user:u-12345\" for per-user budgets, \"team:eng\" for per-team.").optional(), "delivery_preference": z.array(z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"])).describe("Preferred delivery methods, in order of preference.").optional(), "exchanges": z.array(z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260)).describe("Authorized Exchange domains, in the shape \"Request recipient\" defines in the\n file header. Broker queries only these. This is a FILTER over third parties,\n not an address — the recipient of the request carrying it is a separate\n question.").optional(), "max_data_age": z.string().describe("Maximum acceptable age of resource data. The Broker SHOULD\n exclude offers where (now - Offer.data_as_of) exceeds this duration.\n\nOnly relevant for DYNAMIC resources. Ignored for STATIC (content is\n immutable) and LIVE (content doesn't exist yet).\n\n Examples:\n 7 days — \"credit report updated within the last week\"\n 1 hour — \"stock snapshot from the last hour\"\n 30 days — \"drug interaction database updated this month\"").optional(), "max_hops": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Maximum number of hops the agent will allow its discovery to travel. It\n rides only on DiscoveryRequest, which terminates at the Broker, and a\n Broker originates its own queries rather than forwarding the agent's\n request, so no party receives or enforces it in this version: an Exchange\n never sees it, and an Exchange's own cap is\n WellKnownManifest.max_intermediary_hops. The field is kept for multi-hop\n Broker discovery, an open protocol question. Absent = no cap stated.").optional(), "max_price": z.object({ "amount": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Exact decimal string (not a float), e.g. \"19.99\". Denominated in `currency`.").default(""), "currency": z.string().default(""), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).optional() }).describe("Maximum price the agent is willing to pay.").optional(), "max_unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Maximum effective cost per unit, as an exact decimal string (not a float).").optional(), "period_budget": z.object({ "amount": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Exact decimal string (not a float), e.g. \"19.99\". Denominated in `currency`.").default(""), "currency": z.string().default(""), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).optional() }).describe("Per-period budget limit for the budget_scope.").optional(), "preferred_exchanges": z.array(z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260)).describe("Exchanges the agent has existing relationships with (subscriptions,\n contracts). The Broker SHOULD prefer these when resource is\n available — subscription resource has zero marginal cost.").optional(), "reporting_capable": z.boolean().describe("Whether the agent supports post-usage reporting.").optional() }).describe("RequestConstraints — Budget and preference constraints.")))(); -export const RequesterSchema = /* @__PURE__ */ (() => wire(z.object({ "delegation": z.object({ "expires_at": z.string().datetime({ offset: true }).describe("When this delegation expires. Exchange MUST reject expired tokens.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "issuer": z.string().describe("Token issuer. OIDC issuer URL or GNAP grant server URL.\n Exchange uses this for JWT validation (OIDC discovery → JWKS)\n or GNAP token introspection.").optional(), "max_accesses": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Maximum number of accesses allowed under this delegation.\n Exchange tracks cumulative access count against this cap.\n Deny with DENIAL_REASON_QUOTA_EXCEEDED when count >= limit.\n For subscriptions with \"10,000 accesses/month\", this carries the ceiling.").optional(), "max_spend_cents": z.coerce.number().int().describe("Maximum spend in currency minor units (e.g., cents for USD).\n Exchange tracks cumulative spend against this cap.").optional(), "principal_domain": z.string().describe("Who granted this delegation (domain for public key lookup).").default(""), "principal_id": z.string().describe("Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").").default(""), "quota_period": z.string().describe("Quota reset period. How often the access/spend counters reset.\n Example: 30 days for monthly subscriptions — \"2592000s\" on the wire\n (proto-JSON encodes Duration as seconds; \"720h\" is not accepted).\n When absent, the quota is lifetime (bounded only by expires_at).").optional(), "revocation_uri": z.string().describe("Optional: URI for real-time revocation checking.\n Exchange MAY check this for high-value transactions.\n Not checked for routine low-value access (performance tradeoff).").optional(), "scopes": z.array(z.string()).describe("Scopes granted by this delegation. MUST be a subset of the\n principal's own scopes (attenuation — can only narrow, not widen).").optional(), "token": z.string().regex(new RegExp("^[A-Za-z0-9+/]*={0,2}$")).describe("Token bytes. A JWT (base64url-encoded JWS).").default(""), "token_format": z.string().describe("Token format: \"jwt\" (default). Empty is treated as \"jwt\". The field stays\n open for a future format.").default("") }).describe("Optional delegation — present when the requester acts on behalf of\n another entity (user, organization, upstream agent). On a query a Broker\n originated it is not honoured, because the Broker signs alone and nothing\n on that leg can pass its holder binding; the Exchange answers as if none\n were present (see Delegation).").optional(), "domain": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("REQUIRED. Bare host of the requester's key directory: the WBA directory at\n {domain}/.well-known/http-message-signatures-directory that publishes the\n agent's Ed25519 keys. It carries the bare-host shape \"Request recipient\"\n defines in the file header (a port allowed), for the same structural\n reason: a scheme, path or query smuggled in here would choose what gets\n fetched, not merely from where. It is never a free label. Every verifier\n reads it as the name of that directory, and the rule that binds it depends\n on whether the request is direct or relayed, which the receiver decides as\n \"Direct and relayed requests\" in the file header states:\n\n- Direct request. The agent's own RFC 9421 signature arrives because\n the agent sent the request itself. The verifier resolves the agent's\n keys from the COVERED `Signature-Agent` member, never from this field.\n It then MUST require this field to name that same directory: the host\n of the origin that member names, compared by\n the identity rule \"Request recipient\" defines (the shape check first,\n then case-folded, an absent port the same as \":443\", a subdomain a\n different party). A mismatch is refused as UNAUTHENTICATED with\n `request_auth_failure` SIGNATURE_INVALID: the signature verifies, but\n not for the requester the body names. A Broker applies this rule at\n BrokerService.ExecuteTransaction. At BrokerService.Resolve it refuses\n nothing for this field: every query it originates names the host of\n the origin that member names (see DiscoveryRequest.requester). An\n Exchange applies it on every request whose signer it does not admit as\n a Broker; a mismatch from a signer it does admit is a relayed request,\n one of the two cases below.\n - Purchase relayed through a Broker. BrokerService.ExecuteTransaction\n re-packages the purchase, so the request signature and the covered\n `Signature-Agent` member are the Broker's. They say only that the call comes\n from the Broker, and they sign no purchase. The Exchange MUST verify\n each item's AgentAcceptance, and the AgentRequestAcceptance, against\n the Ed25519 keys currently valid in the directory this field names\n (see AgentRequestAcceptance), never against the Broker's key. Those\n acceptances are the only agent signatures the Exchange sees, and the\n purchase belongs to the account of the directory they verify under\n (see \"Accounts\" in the file header).\n - Broker-led discovery. A ResourceQuery a Broker originated carries no agent\n signature, so nothing on that leg authenticates this field: it is the\n Broker's statement, under the Broker's own signature, of whom it\n queries for. A delegation on such a query is not honoured (see\n Delegation)."), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "id": z.string().min(1).max(255).describe("REQUIRED. A free label the agent chooses for attribution (e.g.\n \"agent-research-bot-001\"), 1 to 255 characters. Use it to tell apart the\n sub-agents or end customers that share one key directory. It is never\n identity: no verifier finds keys from it, and no party trusts it for an\n authentication or authorization decision. The requester's identity is\n `domain` together with the key that verifies. The acceptance signatures\n cover this label (see AgentAcceptancePayload), so a relaying Broker cannot\n rewrite it, but its value is still only what the agent says."), "name": z.string().describe("Human-readable name (e.g., \"Acme Research Assistant\").").optional(), "scopes": z.array(z.string()).max(64).describe("Entitlement scopes: the requester's own statement of what it is entitled\n to. On a query a Broker originated, they are the Broker's statement for the\n agent. They are signed and attributable, and the Exchange does not verify\n them. Scope coverage decides which terms the Exchange presents (see\n LicenseTerm.scopes, which also states when an Exchange MAY require a\n verified delegation); it is not access control. A false statement is\n traceable to the signing key, its directory and its account, and the\n agent's acceptance signs the scoped offer it buys; it is settled through\n reconciliation and the account relationship.\n\nEXISTENCE HIDING. A requester never learns about a resource outside its\n scopes. When the scopes leave no presentable offer for a resource, the\n Exchange answers with no offers and no absence reason (an OfferGroup with\n empty `offers` and `absence_reason` unset), exactly as for any resource\n with nothing to offer. A Broker relaying that answer adds no reason of its\n own. At purchase, an offer the Exchange presented is honoured until it\n expires, so a scope refusal cannot arise there. No reason in the protocol\n reports a scope shortfall: OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT and\n DENIAL_REASON_SCOPE_INSUFFICIENT are deprecated and never sent.\n\n Scope format: colon-separated segments, \"{domain}:{permission}\" or\n \"{profile}:{permission}\", optionally multi-segment (\"dist:US:CA\");\n matching is segment-wise per the rule below (no implicit hierarchy).\n Examples:\n \"credit:read\" — can access credit reports\n \"subscription:marketdata-2026\" — has active MarketData subscription\n \"academic:*\" — full access to academic resources\n \"internal:reports\" — can access internal reports\n \"*\" — unrestricted (public Exchange default)\n\n Matching is SEGMENT-WISE (\":\" separated). A granted scope G covers a\n required scope R iff, segment by segment, each G segment equals the\n corresponding R segment or is \"*\"; a terminal \"*\" matches all remaining\n segments. There is NO implicit prefix match, and a grant NARROWER than\n the requirement does not cover it (G must be equal-to-or-broader than R).\n Examples: \"dist:*\" covers \"dist:US\" and \"dist:US:CA\"; \"dist:US:*\" covers\n \"dist:US:CA\" but not \"dist:EU\"; bare \"dist\" covers only \"dist\"; granted\n \"dist:US:CA\" does NOT cover required \"dist:US\"; \"*\" covers everything.\n This same rule governs LicenseTerm.scopes — one algorithm protocol-wide.\n\n When empty, Exchange applies its default access policy (typically\n returns all publicly available resources).").optional(), "type": z.enum(["REQUESTER_TYPE_AGENT","REQUESTER_TYPE_HUMAN_TOOL","REQUESTER_TYPE_SERVICE","REQUESTER_TYPE_DELEGATED","REQUESTER_TYPE_RESEARCH"]).describe("What kind of entity is making this request.") }).describe("Requester — Universal identity for any FORA client.\n\nCarries identity and entitlements ONLY — who is asking and what they are\n entitled to (scopes, delegation). What they are asking for (uris) and the\n limits they will operate within (acceptable_restrictions) belong to the ask,\n not the identity, and live on ResourceQuery / DiscoveryRequest. The Exchange\n verifies identity via the RFC 9421 request signature, then presents the\n terms the requester's stated scopes cover (see LicenseTerm.scopes). Billing\n needs nothing from this message:\n the Exchange resolves the agent's account (RegisterResponse.billing_ref) from\n what authenticates the agent, never from a value the caller merely states\n (see \"Accounts\" in the file header).")))(); +export const RequesterSchema = /* @__PURE__ */ (() => wire(z.object({ "delegation": z.object({ "expires_at": z.string().datetime({ offset: true }).describe("Mirror of the chain's earliest `exp`. The Exchange MUST reject an expired\n token; the signed `exp` values decide, not this field.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "issuer": z.string().describe("Mirror of the ROOT token's `iss`, the identity configured with the trusted\n key (never an intermediate or terminal issuer). Informational: the\n Exchange never discovers a key from it, through OIDC, JWKS or otherwise.").optional(), "max_accesses": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Maximum number of accesses allowed under this delegation.\n Exchange tracks cumulative access count against this cap.\n Deny with DENIAL_REASON_QUOTA_EXCEEDED when count >= limit.\n For subscriptions with \"10,000 accesses/month\", this carries the ceiling.").optional(), "max_spend_cents": z.coerce.number().int().describe("Maximum spend in currency minor units (e.g., cents for USD).\n Exchange tracks cumulative spend against this cap.").optional(), "principal_domain": z.string().describe("Mirror of the ROOT token's `iss`, the configured trust anchor's identity.\n Informational: no key is looked up from it, and it establishes no trust.").default(""), "principal_id": z.string().describe("Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").\n Informational; not read by the verifier.").default(""), "quota_period": z.string().describe("Quota reset period. How often the access/spend counters reset.\n Example: 30 days for monthly subscriptions — \"2592000s\" on the wire\n (proto-JSON encodes Duration as seconds; \"720h\" is not accepted).\n When absent, the quota is lifetime (bounded only by expires_at).").optional(), "revocation_uri": z.string().describe("Informational. Revocation lookup is not part of the delegation profile:\n the verifier never fetches this URI. A chain is invalidated early by\n withdrawing the configured root key.").optional(), "scopes": z.array(z.string()).describe("Mirror of the terminal token's effective scopes. In the chain, each\n child's scopes are covered by its parent's under the scope-matching rule\n on Requester.scopes (attenuation — can only narrow, not widen).").optional(), "token": z.string().regex(new RegExp("^[A-Za-z0-9+/]{0,43691}={0,2}$")).max(43692).describe("Token bytes. A JWT (base64url-encoded JWS). At most 32768 bytes: the\n complete chain, every embedded parent included, must fit. The limit is a\n wire rule, checked before the token is parsed and independent of whether\n the bytes are a valid JWT; a longer token is an invalid request, not a\n per-item denial.").default(""), "token_format": z.string().describe("Token format: \"jwt\" (default). Empty is treated as \"jwt\". The field stays\n open for a future format.").default("") }).describe("Optional delegation — present when the requester acts on behalf of\n another entity (user, organization, upstream agent). On a query a Broker\n originated it is not honoured, because the Broker signs alone and nothing\n on that leg can pass its holder binding; the Exchange answers as if none\n were present (see Delegation).").optional(), "domain": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("REQUIRED. Bare host of the requester's key directory: the WBA directory at\n {domain}/.well-known/http-message-signatures-directory that publishes the\n agent's Ed25519 keys. It carries the bare-host shape \"Request recipient\"\n defines in the file header (a port allowed), for the same structural\n reason: a scheme, path or query smuggled in here would choose what gets\n fetched, not merely from where. It is never a free label. Every verifier\n reads it as the name of that directory, and the rule that binds it depends\n on whether the request is direct or relayed, which the receiver decides as\n \"Direct and relayed requests\" in the file header states:\n\n- Direct request. The agent's own RFC 9421 signature arrives because\n the agent sent the request itself. The verifier resolves the agent's\n keys from the COVERED `Signature-Agent` member, never from this field.\n It then MUST require this field to name that same directory: the host\n of the origin that member names, compared by\n the identity rule \"Request recipient\" defines (the shape check first,\n then case-folded, an absent port the same as \":443\", a subdomain a\n different party). A mismatch is refused as UNAUTHENTICATED with\n `request_auth_failure` SIGNATURE_INVALID: the signature verifies, but\n not for the requester the body names. A Broker applies this rule at\n BrokerService.ExecuteTransaction. At BrokerService.Resolve it refuses\n nothing for this field: every query it originates names the host of\n the origin that member names (see DiscoveryRequest.requester). An\n Exchange applies it on every request whose signer it does not admit as\n a Broker; a mismatch from a signer it does admit is a relayed request,\n one of the two cases below.\n - Purchase relayed through a Broker. BrokerService.ExecuteTransaction\n re-packages the purchase, so the request signature and the covered\n `Signature-Agent` member are the Broker's. They say only that the call comes\n from the Broker, and they sign no purchase. The Exchange MUST verify\n each item's AgentAcceptance, and the AgentRequestAcceptance, against\n the Ed25519 keys currently valid in the directory this field names\n (see AgentRequestAcceptance), never against the Broker's key. Those\n acceptances are the only agent signatures the Exchange sees, and the\n purchase belongs to the account of the directory they verify under\n (see \"Accounts\" in the file header).\n - Broker-led discovery. A ResourceQuery a Broker originated carries no agent\n signature, so nothing on that leg authenticates this field: it is the\n Broker's statement, under the Broker's own signature, of whom it\n queries for. A delegation on such a query is not honoured (see\n Delegation)."), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "id": z.string().min(1).max(255).describe("REQUIRED. A free label the agent chooses for attribution (e.g.\n \"agent-research-bot-001\"), 1 to 255 characters. Use it to tell apart the\n sub-agents or end customers that share one key directory. It is never\n identity: no verifier finds keys from it, and no party trusts it for an\n authentication or authorization decision. The requester's identity is\n `domain` together with the key that verifies. The acceptance signatures\n cover this label (see AgentAcceptancePayload), so a relaying Broker cannot\n rewrite it, but its value is still only what the agent says."), "name": z.string().describe("Human-readable name (e.g., \"Acme Research Assistant\").").optional(), "scopes": z.array(z.string()).max(64).describe("Entitlement scopes: the requester's own statement of what it is entitled\n to. On a query a Broker originated, they are the Broker's statement for the\n agent. They are signed and attributable, and the Exchange does not verify\n them. Scope coverage decides which terms the Exchange presents (see\n LicenseTerm.scopes, which also states when an Exchange MAY require a\n verified delegation); it is not access control. A false statement is\n traceable to the signing key, its directory and its account, and the\n agent's acceptance signs the scoped offer it buys; it is settled through\n reconciliation and the account relationship.\n\nEXISTENCE HIDING. A requester never learns about a resource outside its\n scopes. When the scopes leave no presentable offer for a resource, the\n Exchange answers with no offers and no absence reason (an OfferGroup with\n empty `offers` and `absence_reason` unset), exactly as for any resource\n with nothing to offer. A Broker relaying that answer adds no reason of its\n own. At purchase, an offer the Exchange presented is honoured until it\n expires, so a scope refusal cannot arise there. No reason in the protocol\n reports a scope shortfall: OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT and\n DENIAL_REASON_SCOPE_INSUFFICIENT are deprecated and never sent.\n\n Scope format: colon-separated segments, \"{domain}:{permission}\" or\n \"{profile}:{permission}\", optionally multi-segment (\"dist:US:CA\");\n matching is segment-wise per the rule below (no implicit hierarchy).\n Examples:\n \"credit:read\" — can access credit reports\n \"subscription:marketdata-2026\" — has active MarketData subscription\n \"academic:*\" — full access to academic resources\n \"internal:reports\" — can access internal reports\n \"*\" — unrestricted (public Exchange default)\n\n Matching is SEGMENT-WISE (\":\" separated). A granted scope G covers a\n required scope R iff, segment by segment, each G segment equals the\n corresponding R segment or is \"*\"; a terminal \"*\" matches all remaining\n segments, of which there must be at least one. There is NO implicit prefix\n match, and a grant NARROWER than the requirement does not cover it (G must\n be equal-to-or-broader than R), so a grant with more segments than the\n requirement never covers it, even when its last segment is \"*\". A \"*\" in R\n is a literal segment: a narrow grant never covers a wildcard requirement.\n Matching is case-sensitive and never trims. Examples: \"dist:*\" covers\n \"dist:US\", \"dist:US:CA\" and \"dist:US:*\" but not \"dist\"; \"dist:US:*\" covers\n \"dist:US:CA\" but not \"dist:EU\" or \"dist:US\"; \"publisher:*:read\" covers\n \"publisher:news:read\" but not \"publisher:news:*\"; bare \"dist\" covers only\n \"dist\"; granted \"dist:US:CA\" does NOT cover required \"dist:US\"; \"*\" covers\n everything. This same rule governs LicenseTerm.scopes and the attenuation\n of a delegation child against its parent — one algorithm protocol-wide.\n\n When empty, Exchange applies its default access policy (typically\n returns all publicly available resources).").optional(), "type": z.enum(["REQUESTER_TYPE_AGENT","REQUESTER_TYPE_HUMAN_TOOL","REQUESTER_TYPE_SERVICE","REQUESTER_TYPE_DELEGATED","REQUESTER_TYPE_RESEARCH"]).describe("What kind of entity is making this request.") }).describe("Requester — Universal identity for any FORA client.\n\nCarries identity and entitlements ONLY — who is asking and what they are\n entitled to (scopes, delegation). What they are asking for (uris) and the\n limits they will operate within (acceptable_restrictions) belong to the ask,\n not the identity, and live on ResourceQuery / DiscoveryRequest. The Exchange\n verifies identity via the RFC 9421 request signature, then presents the\n terms the requester's stated scopes cover (see LicenseTerm.scopes). Billing\n needs nothing from this message:\n the Exchange resolves the agent's account (RegisterResponse.billing_ref) from\n what authenticates the agent, never from a value the caller merely states\n (see \"Accounts\" in the file header).")))(); export const RequesterTypeSchema = /* @__PURE__ */ (() => wire(z.enum(["REQUESTER_TYPE_AGENT","REQUESTER_TYPE_HUMAN_TOOL","REQUESTER_TYPE_SERVICE","REQUESTER_TYPE_DELEGATED","REQUESTER_TYPE_RESEARCH"])))(); @@ -162,17 +162,17 @@ export const ResolutionTypeSchema = /* @__PURE__ */ (() => wire(z.enum(["RESOLUT export const ResourceAttestationSchema = /* @__PURE__ */ (() => wire(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"fora.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("ResourceAttestation — Signed envelope of claims from a trusted party.\n\nA provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"fora.attestation.claims_schema\"].")))(); -export const ResourceEntrySchema = /* @__PURE__ */ (() => wire(z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"fora.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("ResourceAttestation — Signed envelope of claims from a trusted party.\n\nA provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"fora.attestation.claims_schema\"].")).max(64).describe("Signed attestations about this resource entry.\n Same semantics as Offer.attestations — see ResourceAttestation message\n for verification levels and claim vocabulary. Attestations pushed via\n CatalogService are verified at push time: the Exchange checks that\n the attestation verifier is authorized to push for this provider\n (via catalog_contributors in the provider's WellKnownManifest) and validates the\n attestation signature against the verifier's public key from its WBA\n directory (the JWK Set at /.well-known/http-message-signatures-directory;\n the keyid is the key's RFC 7638 thumbprint). The verifier's fora.json\n carries only its role, determined by the verifier's operator.").optional(), "content_hash": z.string().max(255).describe("Content hash, carried as the publisher computed it — a bare hex digest or a\n \"method:hexdigest\" form; bounded in length, never format-checked, because\n hash_method names the algorithm.").optional(), "content_id": z.string().max(255).describe("Content identifier").optional(), "domain": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("Provider domain — the bare host the resource lives on, in the shape\n \"Request recipient\" defines in the file header: a port is allowed, a\n scheme, path, query or userinfo is not. With path it forms the catalog URI\n by concatenation, so a value carrying anything but a host would choose the\n URI rather than merely name the host."), "estimated_quantity": z.coerce.number().int().gte(0).lt(2147483648).describe("Estimated quantity in the metering unit").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052): keys within ext\n that the AGENT must understand. The Exchange carries them onto\n Offer.ext_critical and does not consume them, so an unknown key here never\n refuses the push (see \"Critical extensions\" in the file header). Empty\n (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().max(64).describe("Hash algorithm").optional(), "path": z.string().regex(new RegExp("^/[^?#\\x00-\\x20\\x7f]*$")).min(1).max(2048).describe("Content path — an absolute URL path such as \"/premium/article-42.html\":\n starts with \"/\", carries no query or fragment delimiter, no whitespace and\n no control character, and is at most 2048 characters. Characters, not bytes:\n protovalidate's max_len counts Unicode code points, and the pattern admits\n non-ASCII, so a conformant path can exceed 2048 bytes."), "provenance_source": z.string().max(260).describe("Who provided this resource metadata. Creates audit trail for\n \"where did this catalog entry come from?\"").optional(), "provenance_timestamp": z.string().datetime({ offset: true }).describe("When this metadata was collected/generated.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Optional mutability hint. When omitted, the Exchange applies the `STATIC`\n default at Offer build; an explicit `UNSPECIFIED` is rejected. A value in\n `ext` is not read — the typed field is authoritative, so an ext-only value\n is treated as omitted. Mirrors the required Offer-side\n `ResourceIdentity.resource_mutability`.").optional(), "source": z.enum(["INGESTION_SOURCE_FORA_SITEMAP","INGESTION_SOURCE_RSL","INGESTION_SOURCE_SITEMAP","INGESTION_SOURCE_HTML_CRAWL","INGESTION_SOURCE_CMS_API","INGESTION_SOURCE_MANUAL","INGESTION_SOURCE_CATALOG_API"]).describe("How the entry was discovered").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Obligation — A post-use behavioral requirement attached to a LicenseTerm.\n\nExamples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).max(64).describe("Post-use behavioral requirements.\n At most 64, for the reason quotas carries.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. Where it is required depends on the message that\n holds the term, so the requirement is a rule of that message rather than of\n this field:\n - On a catalog term (ResourceEntry.terms) it is REQUIRED regardless of\n semantics — an agent cannot act on a priceless term, so absent Pricing\n is a validation error at ingest (resource_entry.terms.pricing_required).\n model = FREE must be stated explicitly (absent Pricing is not free). A\n REFERENCE_ONLY term states its price here too; its License governs the\n human-readable terms but does not replace the machine-readable price.\n - On the term an offer carries (Offer.terms) it MUST be unset\n (offer.terms.pricing_unset): the offer's price is Offer.pricing, stated\n once.").optional(), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The unit being capped — an open vocabulary axis.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quota — A usage cap the term declares.\n\nA quota states how much a licensee may consume under the term before the\n term expires or must be renegotiated. Like a restriction, it is a declared\n term the parties agree to. Quotas are NOT billing quantities — billing is in\n Pricing.\n\n The metric vocabulary is authored ONLY in the (fora.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).max(64).describe("Usage caps. The agent must not exceed any individual Quota.\n At most 64, the bound every per-message list in this contract carries when\n no rule walks it more than once. It bounds what one term may carry, not the\n work of checking one — a validator walks every element it is handed before\n the cap is reported, so the cost of checking is bounded at the transport.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to. Defined-only: the axis set is\n CLOSED, and a number outside it is refused rather than ignored. A custom\n axis is RESTRICTION_KIND_OTHER, whose meaning rides in permitted/prohibited,\n so a new number was never the extension mechanism — accepting one would\n admit a restriction no consumer can evaluate onto a term whose default is\n BINDING (see advisory below), which fails open on the axis a publisher most\n needs honoured. Closing the axis does NOT bound the cost of the one-per-kind\n rule below, and must not be read as doing so: a number this rule refuses is\n still distinct from every other, so that rule's all() finds no duplicate to\n stop on and walks the list in full anyway. Its cost is bounded by the size\n test the rule itself carries."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restriction — A single constraint on one licensing dimension.\n\nRestrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: they are declared terms the parties agree to when the\n agent accepts the offer. The AGENT self-selects the term whose restrictions it\n can honour and bears compliance. The Exchange never enforces a restriction:\n it is NOT an Exchange-side gate the requester must pass to see or buy a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and by the scopes in the requester's signed\n request — see LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (fora.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — FORA user/organization categories")).max(8).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.\n At most 8, and this list is the one of the three that does NOT carry the\n contract's usual 64: only one restriction per axis is valid, the axis enum\n is defined-only, so four is the longest conformant list and eight leaves\n room for an axis this version does not have. Like the caps on quotas and\n obligations, this one bounds the DOCUMENT — how many restrictions one term\n may carry — and not the work of checking it: a validator walks every element\n it is handed before any cardinality rule is reported, so an over-cap list is\n traversed in full on its way to being refused.\n\nWhat makes this list different is that one rule walks it against ITSELF. The\n one-per-kind rule below is quadratic, so it carries its own size test and\n stays silent above this cap; a conformance guard holds the two numbers equal,\n because a cap raised without the test would leave the lists in between\n unchecked for duplicate axes and accepted. The neighbouring disjointness rule\n on each element is quadratic only in that element's two token lists, both\n capped at 64, so its cost is bounded per restriction and linear across the\n list — it needs no such test.").optional(), "scopes": z.array(z.string()).max(64).describe("Scope-gating: the Exchange presents an offer for this term only when the\n scopes in the requester's signed request (Requester.scopes) cover ALL of\n these scopes (AND-semantics). Empty = public. A subscription term is\n Pricing{model:FREE} + scopes:[\"subscription:...\"].\n\nRequester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n also require a verified delegation that grants these scopes before it\n presents the term. To a requester that presents none, it answers as if the\n term did not exist, as existence hiding requires (see Requester.scopes). An\n offer the Exchange presented is honoured until it expires.\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("LicenseTerm — Universal licensing unit.\n\nOne LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation — with one move: on an offer the\n term's price is Offer.pricing, and the term itself carries none.\n\n Validation rules:\n - Pricing MUST be present on EVERY catalog term (ResourceEntry.terms),\n regardless of semantics — the resource_entry.terms.pricing_required\n rule. Absent Pricing → reject at ingest: an agent cannot act on a term\n with no price. This holds for REFERENCE_ONLY too — its License governs\n the human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - Pricing MUST be unset on the term an offer carries (Offer.terms) — the\n offer.terms.pricing_unset rule. The offer's price is Offer.pricing, and\n each offer derives from exactly one catalog term, so the price is stated\n once and cannot disagree with itself. The requirement is therefore a\n rule of the message that holds the term, not of LicenseTerm.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - A PER_UNIT term MAY state Pricing.estimated_quantity. It is not\n required, on the pushed term or on the offer it projects to — see\n Pricing.estimated_quantity.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).max(32).describe("Publisher-declared licensing terms for this resource.\n See LicenseTerm for the full model. Every term carries its Pricing (the\n resource_entry.terms.pricing_required rule above), whatever its semantics.\n For REFERENCE_ONLY terms, License.uri is authoritative.\n The Exchange validates ENUMERATED terms at push time and surfaces each term\n on its own offer on discovery: the term's price becomes Offer.pricing, and\n the term in Offer.terms carries no pricing. At most 32 terms per entry,\n stated on the wire so every implementation refuses the same size. An over-cap entry refuses the\n whole submission, as every catalog rejection does; what being a wire rule\n changes is WHEN — the refusal now happens at the boundary, before any\n per-entry classification runs, which is why the rejection reason that named\n this cap can no longer be produced for a push.").optional(), "title": z.string().max(512).describe("Content title").optional(), "word_count": z.coerce.number().int().gte(0).lt(2147483648).describe("Word count").optional() }).describe("ResourceEntry — one catalog row as a publisher (or an authorised contributor)\n pushes it. The envelope fields carry their own wire rules, so an entry that\n cannot become a catalog URI is refused at the boundary rather than after\n ingestion; the licensing terms inside carry the LicenseTerm rules. See\n CatalogService for the second, ingest-time tier (token canonicalisation and\n registry membership).\n\nThe list caps here and on LicenseTerm bound HOW MANY, never how large: how many\n terms one entry may carry, how many entries a push can store, and how many paths\n a rejection has to name back. They do not bound the entry's SIZE either — several\n members inside one carry no length rule — and they do NOT bound the work of\n checking a push, and must not be read as doing so — a\n validator walks every element it is handed and reports every violation before\n any cardinality rule is applied, so an over-cap list is fully traversed on its\n way to being refused. The work is bounded one layer down, by the maximum\n request size the recipient will read; a deployment that exposes CatalogService\n sets that cap, and the SDK's server binding sets a default.")))(); +export const ResourceEntrySchema = /* @__PURE__ */ (() => wire(z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"fora.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("ResourceAttestation — Signed envelope of claims from a trusted party.\n\nA provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"fora.attestation.claims_schema\"].")).max(64).describe("Signed attestations about this resource entry.\n Same semantics as Offer.attestations — see ResourceAttestation message\n for verification levels and claim vocabulary. Attestations pushed via\n CatalogService are verified at push time: the Exchange checks that\n the attestation verifier is authorized to push for this provider\n (via catalog_contributors in the provider's WellKnownManifest) and validates the\n attestation signature against the verifier's public key from its WBA\n directory (the JWK Set at /.well-known/http-message-signatures-directory;\n the keyid is the key's RFC 7638 thumbprint). The verifier's fora.json\n carries only its role, determined by the verifier's operator.").optional(), "content_hash": z.string().max(255).describe("Content hash, carried as the publisher computed it — a bare hex digest or a\n \"method:hexdigest\" form; bounded in length, never format-checked, because\n hash_method names the algorithm.").optional(), "content_id": z.string().max(255).describe("Content identifier").optional(), "domain": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("Provider domain — the bare host the resource lives on, in the shape\n \"Request recipient\" defines in the file header: a port is allowed, a\n scheme, path, query or userinfo is not. With path it forms the catalog URI\n by concatenation, so a value carrying anything but a host would choose the\n URI rather than merely name the host."), "estimated_quantity": z.coerce.number().int().gte(0).lt(2147483648).describe("Estimated quantity in the metering unit").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052): keys within ext\n that the AGENT must understand. The Exchange carries them onto\n Offer.ext_critical and does not consume them, so an unknown key here never\n refuses the push (see \"Critical extensions\" in the file header). Empty\n (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().max(64).describe("Hash algorithm").optional(), "path": z.string().regex(new RegExp("^/[^?#\\x00-\\x20\\x7f]*$")).min(1).max(2048).describe("Content path — an absolute URL path such as \"/premium/article-42.html\":\n starts with \"/\", carries no query or fragment delimiter, no whitespace and\n no control character, and is at most 2048 characters. Characters, not bytes:\n protovalidate's max_len counts Unicode code points, and the pattern admits\n non-ASCII, so a conformant path can exceed 2048 bytes."), "provenance_source": z.string().max(260).describe("Who provided this resource metadata. Creates audit trail for\n \"where did this catalog entry come from?\"").optional(), "provenance_timestamp": z.string().datetime({ offset: true }).describe("When this metadata was collected/generated.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Optional mutability hint. When omitted, the Exchange applies the `STATIC`\n default at Offer build; an explicit `UNSPECIFIED` is rejected. A value in\n `ext` is not read — the typed field is authoritative, so an ext-only value\n is treated as omitted. Mirrors the required Offer-side\n `ResourceIdentity.resource_mutability`.").optional(), "source": z.enum(["INGESTION_SOURCE_FORA_SITEMAP","INGESTION_SOURCE_RSL","INGESTION_SOURCE_SITEMAP","INGESTION_SOURCE_HTML_CRAWL","INGESTION_SOURCE_CMS_API","INGESTION_SOURCE_MANUAL","INGESTION_SOURCE_CATALOG_API"]).describe("How the entry was discovered").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Obligation — A post-use behavioral requirement attached to a LicenseTerm.\n\nExamples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).max(64).describe("Post-use behavioral requirements.\n At most 64, for the reason quotas carries.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. Where it is required depends on the message that\n holds the term, so the requirement is a rule of that message rather than of\n this field:\n - On a catalog term (ResourceEntry.terms) it is REQUIRED regardless of\n semantics — an agent cannot act on a priceless term, so absent Pricing\n is a validation error at ingest (resource_entry.terms.pricing_required).\n model = FREE must be stated explicitly (absent Pricing is not free). A\n REFERENCE_ONLY term states its price here too; its License governs the\n human-readable terms but does not replace the machine-readable price.\n - On the term an offer carries (Offer.terms) it MUST be unset\n (offer.terms.pricing_unset): the offer's price is Offer.pricing, stated\n once.").optional(), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The unit being capped — an open vocabulary axis.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quota — A usage cap the term declares.\n\nA quota states how much a licensee may consume under the term before the\n term expires or must be renegotiated. Like a restriction, it is a declared\n term the parties agree to. Quotas are NOT billing quantities — billing is in\n Pricing.\n\n The metric vocabulary is authored ONLY in the (fora.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).max(64).describe("Usage caps. The agent must not exceed any individual Quota.\n At most 64, the bound every per-message list in this contract carries when\n no rule walks it more than once. It bounds what one term may carry, not the\n work of checking one — a validator walks every element it is handed before\n the cap is reported, so the cost of checking is bounded at the transport.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to. Defined-only: the axis set is\n CLOSED, and a number outside it is refused rather than ignored. A custom\n axis is RESTRICTION_KIND_OTHER, whose meaning rides in permitted/prohibited,\n so a new number was never the extension mechanism — accepting one would\n admit a restriction no consumer can evaluate onto a term whose default is\n BINDING (see advisory below), which fails open on the axis a publisher most\n needs honoured. Closing the axis does NOT bound the cost of the one-per-kind\n rule below, and must not be read as doing so: a number this rule refuses is\n still distinct from every other, so that rule's all() finds no duplicate to\n stop on and walks the list in full anyway. Its cost is bounded by the size\n test the rule itself carries."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restriction — A single constraint on one licensing dimension.\n\nRestrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: they are declared terms the parties agree to when the\n agent accepts the offer. The AGENT self-selects the term whose restrictions it\n can honour and bears compliance. The Exchange never enforces a restriction:\n it is NOT an Exchange-side gate the requester must pass to see or buy a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and by the scopes in the requester's signed\n request — see LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (fora.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — FORA user/organization categories")).max(8).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.\n At most 8, and this list is the one of the three that does NOT carry the\n contract's usual 64: only one restriction per axis is valid, the axis enum\n is defined-only, so four is the longest conformant list and eight leaves\n room for an axis this version does not have. Like the caps on quotas and\n obligations, this one bounds the DOCUMENT — how many restrictions one term\n may carry — and not the work of checking it: a validator walks every element\n it is handed before any cardinality rule is reported, so an over-cap list is\n traversed in full on its way to being refused.\n\nWhat makes this list different is that one rule walks it against ITSELF. The\n one-per-kind rule below is quadratic, so it carries its own size test and\n stays silent above this cap; a conformance guard holds the two numbers equal,\n because a cap raised without the test would leave the lists in between\n unchecked for duplicate axes and accepted. The neighbouring disjointness rule\n on each element is quadratic only in that element's two token lists, both\n capped at 64, so its cost is bounded per restriction and linear across the\n list — it needs no such test.").optional(), "scopes": z.array(z.string()).max(64).describe("Scope-gating: the Exchange presents an offer for this term only when the\n scopes in the requester's signed request (Requester.scopes) cover ALL of\n these scopes (AND-semantics). Empty = public. A subscription term is\n Pricing{model:FREE} + scopes:[\"subscription:...\"].\n\nRequester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n instead gate a term behind a verified delegation (a subscription term is\n the usual case): then only the delegation chain's verified effective scopes\n are matched against these scopes, and Requester.scopes is ignored for that\n term. To a requester that presents no valid delegation, it answers as if\n the term did not exist, as existence hiding requires (see\n Requester.scopes). An offer the Exchange presented is honoured until it\n expires, and a gated offer expires no later than the chain's effective\n expiry (see Delegation).\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments (at least one), and there is NO implicit prefix match\n (a grant narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("LicenseTerm — Universal licensing unit.\n\nOne LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation — with one move: on an offer the\n term's price is Offer.pricing, and the term itself carries none.\n\n Validation rules:\n - Pricing MUST be present on EVERY catalog term (ResourceEntry.terms),\n regardless of semantics — the resource_entry.terms.pricing_required\n rule. Absent Pricing → reject at ingest: an agent cannot act on a term\n with no price. This holds for REFERENCE_ONLY too — its License governs\n the human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - Pricing MUST be unset on the term an offer carries (Offer.terms) — the\n offer.terms.pricing_unset rule. The offer's price is Offer.pricing, and\n each offer derives from exactly one catalog term, so the price is stated\n once and cannot disagree with itself. The requirement is therefore a\n rule of the message that holds the term, not of LicenseTerm.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - A PER_UNIT term MAY state Pricing.estimated_quantity. It is not\n required, on the pushed term or on the offer it projects to — see\n Pricing.estimated_quantity.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).max(32).describe("Publisher-declared licensing terms for this resource.\n See LicenseTerm for the full model. Every term carries its Pricing (the\n resource_entry.terms.pricing_required rule above), whatever its semantics.\n For REFERENCE_ONLY terms, License.uri is authoritative.\n The Exchange validates ENUMERATED terms at push time and surfaces each term\n on its own offer on discovery: the term's price becomes Offer.pricing, and\n the term in Offer.terms carries no pricing. At most 32 terms per entry,\n stated on the wire so every implementation refuses the same size. An over-cap entry refuses the\n whole submission, as every catalog rejection does; what being a wire rule\n changes is WHEN — the refusal now happens at the boundary, before any\n per-entry classification runs, which is why the rejection reason that named\n this cap can no longer be produced for a push.").optional(), "title": z.string().max(512).describe("Content title").optional(), "word_count": z.coerce.number().int().gte(0).lt(2147483648).describe("Word count").optional() }).describe("ResourceEntry — one catalog row as a publisher (or an authorised contributor)\n pushes it. The envelope fields carry their own wire rules, so an entry that\n cannot become a catalog URI is refused at the boundary rather than after\n ingestion; the licensing terms inside carry the LicenseTerm rules. See\n CatalogService for the second, ingest-time tier (token canonicalisation and\n registry membership).\n\nThe list caps here and on LicenseTerm bound HOW MANY, never how large: how many\n terms one entry may carry, how many entries a push can store, and how many paths\n a rejection has to name back. They do not bound the entry's SIZE either — several\n members inside one carry no length rule — and they do NOT bound the work of\n checking a push, and must not be read as doing so — a\n validator walks every element it is handed and reports every violation before\n any cardinality rule is applied, so an over-cap list is fully traversed on its\n way to being refused. The work is bounded one layer down, by the maximum\n request size the recipient will read; a deployment that exposes CatalogService\n sets that cap, and the SDK's server binding sets a default.")))(); export const ResourceIdentitySchema = /* @__PURE__ */ (() => wire(z.object({ "c2pa_manifest": z.string().describe("C2PA content credentials manifest URI.\n Points to a sidecar or embedded C2PA manifest for this resource.\n C2PA-aware agents MAY follow this URI to validate the full provenance\n chain (creator identity, transformation history, ingredient composition)\n using C2PA libraries (JUMBF/COSE Sign1). C2PA-unaware agents can rely\n on c2pa_status and c2pa-bridged attestation claims instead.\n\nFormats:\n Sidecar: HTTPS URI to a .c2pa manifest file\n Embedded: same URI as canonical_url (manifest is inside the asset)\n Content Credentials Cloud: https://contentcredentials.org/verify?uri=...").optional(), "c2pa_status": z.enum(["C2PA_STATUS_TRUSTED","C2PA_STATUS_VALID","C2PA_STATUS_INVALID","C2PA_STATUS_ABSENT"]).describe("The full C2PA validation details (signer identity, trust list,\n action history, training/mining status) are carried in a\n ResourceAttestation with c2pa.* claims — see fora-c2pa-v1 profile.").optional(), "canonical_url": z.string().describe("Provider's authoritative URL for this resource (rel=\"canonical\").\n Always available. Different per provider for syndicated content.").optional(), "content_hash": z.string().describe("Hash of the content. Interpretation depends on hash_method:\n \"simhash-v1\" → locality-sensitive hash, for fuzzy dedup (Level 1)\n \"sha256\" → exact-match integrity hash (Level 2)\n\nLevel 1 (SimHash): computed by Exchange from extracted text.\n Agent verifies that fetched content is \"substantially similar.\"\n Tolerates dynamic page elements.\n\n Level 2 (SHA-256): computed by provider from deterministic payload.\n Agent verifies exact match. Requires provider to serve consistent\n content (e.g., API endpoint, static HTML, structured JSON).\n Mismatch = dispute. Commands premium pricing.").optional(), "doi": z.string().describe("Digital Object Identifier — persistent, never changes.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().describe("Hash algorithm and verification level.\n Examples: \"simhash-v1\", \"minhash-v1\", \"sha256\", \"sha384\"").optional(), "iptc_guid": z.string().describe("IPTC NewsML-G2 globally unique identifier.\n Present when resource flows through news wire syndication (AP, Reuters).").optional(), "isni": z.string().describe("International Standard Name Identifier for the creator.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Drives hash verification behavior:\n STATIC: content_hash is stable. Agent SHOULD verify delivered content matches.\n DYNAMIC: content changes between offer and fetch (credit reports, drug databases).\n content_hash reflects state at offer generation time. Hash mismatch is\n expected and MUST NOT trigger automatic dispute.\n LIVE: content does not exist at offer time (streaming feeds, live broadcasts).\n content_hash is not applicable. The \"resource\" is the stream endpoint.\n\n Validated across 18 use cases: static content (articles, patents, legislation),\n dynamic data (credit reports, drug interactions, stock snapshots), and live\n streams (MarketData quotes, NPR broadcast, news monitoring feeds)."), "soft_binding": z.string().describe("Soft binding hash — content-derived identifier that survives format\n transcoding (resolution changes, compression, PDF-to-text extraction).\n Extracted from C2PA soft binding assertion when present.\n Enables post-delivery verification when the hard binding hash breaks\n due to legitimate format conversion.\n\nAlgorithm specified in soft_binding_method. Values are algorithm-specific\n (e.g., perceptual hash hex string, watermark identifier).").optional(), "soft_binding_method": z.string().describe("Algorithm used for soft_binding.\n Examples: \"phash-v1\" (perceptual hash), \"c2pa-watermark\" (C2PA invisible\n watermark), \"chromaprint\" (audio fingerprint).").optional() }).describe("ResourceIdentity — Layered resource identification and verification.\n\nServes two purposes:\n 1. Cross-exchange deduplication (Broker groups offers for\n the same underlying resource to compare pricing).\n 2. Resource integrity verification (agent checks that delivered\n resource matches what was promised).\n\n Providers declare what verification level they support via\n content_hash + hash_method. Higher verification = higher trust =\n resource can command premium pricing.\n\n Level 0: No hash (canonical_url only). Agent gets what it gets.\n Level 1: SimHash (fuzzy match). Tolerates minor page changes\n (ads, nav, timestamps). Catches bait-and-switch.\n Level 2: SHA-256 (exact match). Provider serves a clean,\n deterministic payload. Agent verifies exact content.\n\n This is non-blocking on current infrastructure: providers who\n serve dynamic pages use Level 0-1. Providers who invest in\n consistent resource delivery reach Level 2 and earn more.\n\n CoMP's Package.id identifies the *package* (exchange-specific);\n ResourceIdentity identifies the *resource* (cross-exchange).")))(); export const ResourceMutabilitySchema = /* @__PURE__ */ (() => wire(z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"])))(); -export const ResourceQuerySchema = /* @__PURE__ */ (() => wire(z.object({ "acceptable_restrictions": z.array(z.object({ "axis": z.union([z.string().regex(new RegExp("^RESTRICTION_KIND_UNSPECIFIED$")), z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("Which axis (same enum as Restriction.kind): FUNCTION / GEOGRAPHY /\n USER_TYPE / OTHER.").default(0), "values": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("The values the query operates within on this axis — same token vocabulary\n as the terms (e.g. FUNCTION [\"ai-train\"], GEOGRAPHY [\"US\", \"EU\"]).").optional() }).describe("AcceptableRestriction — the limits a query operates within on one restriction\n axis, expressed in the same RestrictionKind vocabulary that terms use. The\n Exchange/Broker MAY pre-select offers whose term restrictions fall within\n these as a convenience (see Restriction); it is NOT enforcement — the agent\n self-selects and bears compliance.")).describe("The limits this query operates within, per restriction axis (function,\n geography, user-type, …) — see AcceptableRestriction. Advisory selection\n inputs the Exchange/Broker MAY pre-select offers against (convenience, not\n enforcement); the agent self-selects and bears compliance.").optional(), "deadline": z.string().describe("Maximum time the caller will wait for a response.\n Exchange SHOULD prioritize speed over completeness when tight.\n Absent = \"0.5s\" default (proto-JSON encodes Duration as seconds).").optional(), "exchange": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("REQUIRED. Bare host of the recipient this request is addressed to (e.g.\n \"exchange.example\" or \"exchange.example:8081\"). See \"Request recipient\" in\n the file header for the full contract, including the recipient's duty to\n reject a request that names someone else."), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "requester": z.object({ "delegation": z.object({ "expires_at": z.string().datetime({ offset: true }).describe("When this delegation expires. Exchange MUST reject expired tokens.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "issuer": z.string().describe("Token issuer. OIDC issuer URL or GNAP grant server URL.\n Exchange uses this for JWT validation (OIDC discovery → JWKS)\n or GNAP token introspection.").optional(), "max_accesses": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Maximum number of accesses allowed under this delegation.\n Exchange tracks cumulative access count against this cap.\n Deny with DENIAL_REASON_QUOTA_EXCEEDED when count >= limit.\n For subscriptions with \"10,000 accesses/month\", this carries the ceiling.").optional(), "max_spend_cents": z.coerce.number().int().describe("Maximum spend in currency minor units (e.g., cents for USD).\n Exchange tracks cumulative spend against this cap.").optional(), "principal_domain": z.string().describe("Who granted this delegation (domain for public key lookup).").default(""), "principal_id": z.string().describe("Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").").default(""), "quota_period": z.string().describe("Quota reset period. How often the access/spend counters reset.\n Example: 30 days for monthly subscriptions — \"2592000s\" on the wire\n (proto-JSON encodes Duration as seconds; \"720h\" is not accepted).\n When absent, the quota is lifetime (bounded only by expires_at).").optional(), "revocation_uri": z.string().describe("Optional: URI for real-time revocation checking.\n Exchange MAY check this for high-value transactions.\n Not checked for routine low-value access (performance tradeoff).").optional(), "scopes": z.array(z.string()).describe("Scopes granted by this delegation. MUST be a subset of the\n principal's own scopes (attenuation — can only narrow, not widen).").optional(), "token": z.string().regex(new RegExp("^[A-Za-z0-9+/]*={0,2}$")).describe("Token bytes. A JWT (base64url-encoded JWS).").default(""), "token_format": z.string().describe("Token format: \"jwt\" (default). Empty is treated as \"jwt\". The field stays\n open for a future format.").default("") }).describe("Optional delegation — present when the requester acts on behalf of\n another entity (user, organization, upstream agent). On a query a Broker\n originated it is not honoured, because the Broker signs alone and nothing\n on that leg can pass its holder binding; the Exchange answers as if none\n were present (see Delegation).").optional(), "domain": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("REQUIRED. Bare host of the requester's key directory: the WBA directory at\n {domain}/.well-known/http-message-signatures-directory that publishes the\n agent's Ed25519 keys. It carries the bare-host shape \"Request recipient\"\n defines in the file header (a port allowed), for the same structural\n reason: a scheme, path or query smuggled in here would choose what gets\n fetched, not merely from where. It is never a free label. Every verifier\n reads it as the name of that directory, and the rule that binds it depends\n on whether the request is direct or relayed, which the receiver decides as\n \"Direct and relayed requests\" in the file header states:\n\n- Direct request. The agent's own RFC 9421 signature arrives because\n the agent sent the request itself. The verifier resolves the agent's\n keys from the COVERED `Signature-Agent` member, never from this field.\n It then MUST require this field to name that same directory: the host\n of the origin that member names, compared by\n the identity rule \"Request recipient\" defines (the shape check first,\n then case-folded, an absent port the same as \":443\", a subdomain a\n different party). A mismatch is refused as UNAUTHENTICATED with\n `request_auth_failure` SIGNATURE_INVALID: the signature verifies, but\n not for the requester the body names. A Broker applies this rule at\n BrokerService.ExecuteTransaction. At BrokerService.Resolve it refuses\n nothing for this field: every query it originates names the host of\n the origin that member names (see DiscoveryRequest.requester). An\n Exchange applies it on every request whose signer it does not admit as\n a Broker; a mismatch from a signer it does admit is a relayed request,\n one of the two cases below.\n - Purchase relayed through a Broker. BrokerService.ExecuteTransaction\n re-packages the purchase, so the request signature and the covered\n `Signature-Agent` member are the Broker's. They say only that the call comes\n from the Broker, and they sign no purchase. The Exchange MUST verify\n each item's AgentAcceptance, and the AgentRequestAcceptance, against\n the Ed25519 keys currently valid in the directory this field names\n (see AgentRequestAcceptance), never against the Broker's key. Those\n acceptances are the only agent signatures the Exchange sees, and the\n purchase belongs to the account of the directory they verify under\n (see \"Accounts\" in the file header).\n - Broker-led discovery. A ResourceQuery a Broker originated carries no agent\n signature, so nothing on that leg authenticates this field: it is the\n Broker's statement, under the Broker's own signature, of whom it\n queries for. A delegation on such a query is not honoured (see\n Delegation)."), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "id": z.string().min(1).max(255).describe("REQUIRED. A free label the agent chooses for attribution (e.g.\n \"agent-research-bot-001\"), 1 to 255 characters. Use it to tell apart the\n sub-agents or end customers that share one key directory. It is never\n identity: no verifier finds keys from it, and no party trusts it for an\n authentication or authorization decision. The requester's identity is\n `domain` together with the key that verifies. The acceptance signatures\n cover this label (see AgentAcceptancePayload), so a relaying Broker cannot\n rewrite it, but its value is still only what the agent says."), "name": z.string().describe("Human-readable name (e.g., \"Acme Research Assistant\").").optional(), "scopes": z.array(z.string()).max(64).describe("Entitlement scopes: the requester's own statement of what it is entitled\n to. On a query a Broker originated, they are the Broker's statement for the\n agent. They are signed and attributable, and the Exchange does not verify\n them. Scope coverage decides which terms the Exchange presents (see\n LicenseTerm.scopes, which also states when an Exchange MAY require a\n verified delegation); it is not access control. A false statement is\n traceable to the signing key, its directory and its account, and the\n agent's acceptance signs the scoped offer it buys; it is settled through\n reconciliation and the account relationship.\n\nEXISTENCE HIDING. A requester never learns about a resource outside its\n scopes. When the scopes leave no presentable offer for a resource, the\n Exchange answers with no offers and no absence reason (an OfferGroup with\n empty `offers` and `absence_reason` unset), exactly as for any resource\n with nothing to offer. A Broker relaying that answer adds no reason of its\n own. At purchase, an offer the Exchange presented is honoured until it\n expires, so a scope refusal cannot arise there. No reason in the protocol\n reports a scope shortfall: OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT and\n DENIAL_REASON_SCOPE_INSUFFICIENT are deprecated and never sent.\n\n Scope format: colon-separated segments, \"{domain}:{permission}\" or\n \"{profile}:{permission}\", optionally multi-segment (\"dist:US:CA\");\n matching is segment-wise per the rule below (no implicit hierarchy).\n Examples:\n \"credit:read\" — can access credit reports\n \"subscription:marketdata-2026\" — has active MarketData subscription\n \"academic:*\" — full access to academic resources\n \"internal:reports\" — can access internal reports\n \"*\" — unrestricted (public Exchange default)\n\n Matching is SEGMENT-WISE (\":\" separated). A granted scope G covers a\n required scope R iff, segment by segment, each G segment equals the\n corresponding R segment or is \"*\"; a terminal \"*\" matches all remaining\n segments. There is NO implicit prefix match, and a grant NARROWER than\n the requirement does not cover it (G must be equal-to-or-broader than R).\n Examples: \"dist:*\" covers \"dist:US\" and \"dist:US:CA\"; \"dist:US:*\" covers\n \"dist:US:CA\" but not \"dist:EU\"; bare \"dist\" covers only \"dist\"; granted\n \"dist:US:CA\" does NOT cover required \"dist:US\"; \"*\" covers everything.\n This same rule governs LicenseTerm.scopes — one algorithm protocol-wide.\n\n When empty, Exchange applies its default access policy (typically\n returns all publicly available resources).").optional(), "type": z.enum(["REQUESTER_TYPE_AGENT","REQUESTER_TYPE_HUMAN_TOOL","REQUESTER_TYPE_SERVICE","REQUESTER_TYPE_DELEGATED","REQUESTER_TYPE_RESEARCH"]).describe("What kind of entity is making this request.") }).describe("Requester identity — who is making this request, the scopes it states,\n and optional delegation chain. REQUIRED: a query names its requester, whose\n Requester.id and Requester.domain are required in turn."), "supported_profiles": z.array(z.string()).describe("Domain extension profiles the caller understands.\n\nDeclares which ext field vocabularies the caller can parse and act on.\n The Exchange SHOULD include profile-specific ext fields in Offers\n when the caller declares support. The Exchange MAY skip expensive\n metadata computation (e.g., retraction checking, consolidation\n verification) when the caller does not declare the relevant profile.\n\n Absence means \"send all available metadata\" — Exchange MUST NOT\n withhold ext fields solely because the caller omitted this field.\n\n Values match the Exchange's WellKnownManifest.supported_profiles entries.\n Examples: [\"fora-news-v1\", \"fora-academic-v1\", \"fora-legal-v1\"]").optional(), "uris": z.array(z.string()).max(256).describe("Resource URIs being queried.").optional(), "ver": z.string().describe("FORA protocol version — \"1.0\". Stamped by the sender from a single\n constant; advisory on receive. See \"Protocol version\" in the file header.").default("") }).describe("ResourceQuery — Query an Exchange for available resource offers.\n\nSent by a Broker or directly by an AI agent.\n The Exchange evaluates its access policies, available inventory,\n and reporting requirements before responding.")))(); +export const ResourceQuerySchema = /* @__PURE__ */ (() => wire(z.object({ "acceptable_restrictions": z.array(z.object({ "axis": z.union([z.string().regex(new RegExp("^RESTRICTION_KIND_UNSPECIFIED$")), z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("Which axis (same enum as Restriction.kind): FUNCTION / GEOGRAPHY /\n USER_TYPE / OTHER.").default(0), "values": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("The values the query operates within on this axis — same token vocabulary\n as the terms (e.g. FUNCTION [\"ai-train\"], GEOGRAPHY [\"US\", \"EU\"]).").optional() }).describe("AcceptableRestriction — the limits a query operates within on one restriction\n axis, expressed in the same RestrictionKind vocabulary that terms use. The\n Exchange/Broker MAY pre-select offers whose term restrictions fall within\n these as a convenience (see Restriction); it is NOT enforcement — the agent\n self-selects and bears compliance.")).describe("The limits this query operates within, per restriction axis (function,\n geography, user-type, …) — see AcceptableRestriction. Advisory selection\n inputs the Exchange/Broker MAY pre-select offers against (convenience, not\n enforcement); the agent self-selects and bears compliance.").optional(), "deadline": z.string().describe("Maximum time the caller will wait for a response.\n Exchange SHOULD prioritize speed over completeness when tight.\n Absent = \"0.5s\" default (proto-JSON encodes Duration as seconds).").optional(), "exchange": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("REQUIRED. Bare host of the recipient this request is addressed to (e.g.\n \"exchange.example\" or \"exchange.example:8081\"). See \"Request recipient\" in\n the file header for the full contract, including the recipient's duty to\n reject a request that names someone else."), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "requester": z.object({ "delegation": z.object({ "expires_at": z.string().datetime({ offset: true }).describe("Mirror of the chain's earliest `exp`. The Exchange MUST reject an expired\n token; the signed `exp` values decide, not this field.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "issuer": z.string().describe("Mirror of the ROOT token's `iss`, the identity configured with the trusted\n key (never an intermediate or terminal issuer). Informational: the\n Exchange never discovers a key from it, through OIDC, JWKS or otherwise.").optional(), "max_accesses": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Maximum number of accesses allowed under this delegation.\n Exchange tracks cumulative access count against this cap.\n Deny with DENIAL_REASON_QUOTA_EXCEEDED when count >= limit.\n For subscriptions with \"10,000 accesses/month\", this carries the ceiling.").optional(), "max_spend_cents": z.coerce.number().int().describe("Maximum spend in currency minor units (e.g., cents for USD).\n Exchange tracks cumulative spend against this cap.").optional(), "principal_domain": z.string().describe("Mirror of the ROOT token's `iss`, the configured trust anchor's identity.\n Informational: no key is looked up from it, and it establishes no trust.").default(""), "principal_id": z.string().describe("Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").\n Informational; not read by the verifier.").default(""), "quota_period": z.string().describe("Quota reset period. How often the access/spend counters reset.\n Example: 30 days for monthly subscriptions — \"2592000s\" on the wire\n (proto-JSON encodes Duration as seconds; \"720h\" is not accepted).\n When absent, the quota is lifetime (bounded only by expires_at).").optional(), "revocation_uri": z.string().describe("Informational. Revocation lookup is not part of the delegation profile:\n the verifier never fetches this URI. A chain is invalidated early by\n withdrawing the configured root key.").optional(), "scopes": z.array(z.string()).describe("Mirror of the terminal token's effective scopes. In the chain, each\n child's scopes are covered by its parent's under the scope-matching rule\n on Requester.scopes (attenuation — can only narrow, not widen).").optional(), "token": z.string().regex(new RegExp("^[A-Za-z0-9+/]{0,43691}={0,2}$")).max(43692).describe("Token bytes. A JWT (base64url-encoded JWS). At most 32768 bytes: the\n complete chain, every embedded parent included, must fit. The limit is a\n wire rule, checked before the token is parsed and independent of whether\n the bytes are a valid JWT; a longer token is an invalid request, not a\n per-item denial.").default(""), "token_format": z.string().describe("Token format: \"jwt\" (default). Empty is treated as \"jwt\". The field stays\n open for a future format.").default("") }).describe("Optional delegation — present when the requester acts on behalf of\n another entity (user, organization, upstream agent). On a query a Broker\n originated it is not honoured, because the Broker signs alone and nothing\n on that leg can pass its holder binding; the Exchange answers as if none\n were present (see Delegation).").optional(), "domain": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("REQUIRED. Bare host of the requester's key directory: the WBA directory at\n {domain}/.well-known/http-message-signatures-directory that publishes the\n agent's Ed25519 keys. It carries the bare-host shape \"Request recipient\"\n defines in the file header (a port allowed), for the same structural\n reason: a scheme, path or query smuggled in here would choose what gets\n fetched, not merely from where. It is never a free label. Every verifier\n reads it as the name of that directory, and the rule that binds it depends\n on whether the request is direct or relayed, which the receiver decides as\n \"Direct and relayed requests\" in the file header states:\n\n- Direct request. The agent's own RFC 9421 signature arrives because\n the agent sent the request itself. The verifier resolves the agent's\n keys from the COVERED `Signature-Agent` member, never from this field.\n It then MUST require this field to name that same directory: the host\n of the origin that member names, compared by\n the identity rule \"Request recipient\" defines (the shape check first,\n then case-folded, an absent port the same as \":443\", a subdomain a\n different party). A mismatch is refused as UNAUTHENTICATED with\n `request_auth_failure` SIGNATURE_INVALID: the signature verifies, but\n not for the requester the body names. A Broker applies this rule at\n BrokerService.ExecuteTransaction. At BrokerService.Resolve it refuses\n nothing for this field: every query it originates names the host of\n the origin that member names (see DiscoveryRequest.requester). An\n Exchange applies it on every request whose signer it does not admit as\n a Broker; a mismatch from a signer it does admit is a relayed request,\n one of the two cases below.\n - Purchase relayed through a Broker. BrokerService.ExecuteTransaction\n re-packages the purchase, so the request signature and the covered\n `Signature-Agent` member are the Broker's. They say only that the call comes\n from the Broker, and they sign no purchase. The Exchange MUST verify\n each item's AgentAcceptance, and the AgentRequestAcceptance, against\n the Ed25519 keys currently valid in the directory this field names\n (see AgentRequestAcceptance), never against the Broker's key. Those\n acceptances are the only agent signatures the Exchange sees, and the\n purchase belongs to the account of the directory they verify under\n (see \"Accounts\" in the file header).\n - Broker-led discovery. A ResourceQuery a Broker originated carries no agent\n signature, so nothing on that leg authenticates this field: it is the\n Broker's statement, under the Broker's own signature, of whom it\n queries for. A delegation on such a query is not honoured (see\n Delegation)."), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "id": z.string().min(1).max(255).describe("REQUIRED. A free label the agent chooses for attribution (e.g.\n \"agent-research-bot-001\"), 1 to 255 characters. Use it to tell apart the\n sub-agents or end customers that share one key directory. It is never\n identity: no verifier finds keys from it, and no party trusts it for an\n authentication or authorization decision. The requester's identity is\n `domain` together with the key that verifies. The acceptance signatures\n cover this label (see AgentAcceptancePayload), so a relaying Broker cannot\n rewrite it, but its value is still only what the agent says."), "name": z.string().describe("Human-readable name (e.g., \"Acme Research Assistant\").").optional(), "scopes": z.array(z.string()).max(64).describe("Entitlement scopes: the requester's own statement of what it is entitled\n to. On a query a Broker originated, they are the Broker's statement for the\n agent. They are signed and attributable, and the Exchange does not verify\n them. Scope coverage decides which terms the Exchange presents (see\n LicenseTerm.scopes, which also states when an Exchange MAY require a\n verified delegation); it is not access control. A false statement is\n traceable to the signing key, its directory and its account, and the\n agent's acceptance signs the scoped offer it buys; it is settled through\n reconciliation and the account relationship.\n\nEXISTENCE HIDING. A requester never learns about a resource outside its\n scopes. When the scopes leave no presentable offer for a resource, the\n Exchange answers with no offers and no absence reason (an OfferGroup with\n empty `offers` and `absence_reason` unset), exactly as for any resource\n with nothing to offer. A Broker relaying that answer adds no reason of its\n own. At purchase, an offer the Exchange presented is honoured until it\n expires, so a scope refusal cannot arise there. No reason in the protocol\n reports a scope shortfall: OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT and\n DENIAL_REASON_SCOPE_INSUFFICIENT are deprecated and never sent.\n\n Scope format: colon-separated segments, \"{domain}:{permission}\" or\n \"{profile}:{permission}\", optionally multi-segment (\"dist:US:CA\");\n matching is segment-wise per the rule below (no implicit hierarchy).\n Examples:\n \"credit:read\" — can access credit reports\n \"subscription:marketdata-2026\" — has active MarketData subscription\n \"academic:*\" — full access to academic resources\n \"internal:reports\" — can access internal reports\n \"*\" — unrestricted (public Exchange default)\n\n Matching is SEGMENT-WISE (\":\" separated). A granted scope G covers a\n required scope R iff, segment by segment, each G segment equals the\n corresponding R segment or is \"*\"; a terminal \"*\" matches all remaining\n segments, of which there must be at least one. There is NO implicit prefix\n match, and a grant NARROWER than the requirement does not cover it (G must\n be equal-to-or-broader than R), so a grant with more segments than the\n requirement never covers it, even when its last segment is \"*\". A \"*\" in R\n is a literal segment: a narrow grant never covers a wildcard requirement.\n Matching is case-sensitive and never trims. Examples: \"dist:*\" covers\n \"dist:US\", \"dist:US:CA\" and \"dist:US:*\" but not \"dist\"; \"dist:US:*\" covers\n \"dist:US:CA\" but not \"dist:EU\" or \"dist:US\"; \"publisher:*:read\" covers\n \"publisher:news:read\" but not \"publisher:news:*\"; bare \"dist\" covers only\n \"dist\"; granted \"dist:US:CA\" does NOT cover required \"dist:US\"; \"*\" covers\n everything. This same rule governs LicenseTerm.scopes and the attenuation\n of a delegation child against its parent — one algorithm protocol-wide.\n\n When empty, Exchange applies its default access policy (typically\n returns all publicly available resources).").optional(), "type": z.enum(["REQUESTER_TYPE_AGENT","REQUESTER_TYPE_HUMAN_TOOL","REQUESTER_TYPE_SERVICE","REQUESTER_TYPE_DELEGATED","REQUESTER_TYPE_RESEARCH"]).describe("What kind of entity is making this request.") }).describe("Requester identity — who is making this request, the scopes it states,\n and optional delegation chain. REQUIRED: a query names its requester, whose\n Requester.id and Requester.domain are required in turn."), "supported_profiles": z.array(z.string()).describe("Domain extension profiles the caller understands.\n\nDeclares which ext field vocabularies the caller can parse and act on.\n The Exchange SHOULD include profile-specific ext fields in Offers\n when the caller declares support. The Exchange MAY skip expensive\n metadata computation (e.g., retraction checking, consolidation\n verification) when the caller does not declare the relevant profile.\n\n Absence means \"send all available metadata\" — Exchange MUST NOT\n withhold ext fields solely because the caller omitted this field.\n\n Values match the Exchange's WellKnownManifest.supported_profiles entries.\n Examples: [\"fora-news-v1\", \"fora-academic-v1\", \"fora-legal-v1\"]").optional(), "uris": z.array(z.string()).max(256).describe("Resource URIs being queried.").optional(), "ver": z.string().describe("FORA protocol version — \"1.0\". Stamped by the sender from a single\n constant; advisory on receive. See \"Protocol version\" in the file header.").default("") }).describe("ResourceQuery — Query an Exchange for available resource offers.\n\nSent by a Broker or directly by an AI agent.\n The Exchange evaluates its access policies, available inventory,\n and reporting requirements before responding.")))(); export const ResourceRefSchema = /* @__PURE__ */ (() => wire(z.object({ "domain": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("The bare host the resource lives on, as ResourceEntry.domain."), "path": z.string().regex(new RegExp("^/[^?#\\x00-\\x20\\x7f]*$")).min(1).max(2048).describe("The resource's absolute path, as ResourceEntry.path.") }).describe("ResourceRef — one catalog resource, named the way PushResources names it:\n by ResourceEntry.domain and ResourceEntry.path, under the same rules, which\n together form the catalog URI.")))(); -export const ResourceResponseSchema = /* @__PURE__ */ (() => wire(z.object({ "exchange": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("Canonical domain of the responding Exchange, in the shape \"Request\n recipient\" defines in the file header. The response counterpart of the\n recipient field on the request: it names who answered."), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "offer_groups": z.array(z.object({ "absence_reason": z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"]).describe("Why no offers are available for this URI.\n Set only when `offers` is empty, and not always then: a resource the\n requester's scopes leave with no presentable offer is answered with empty\n `offers` and this field unset (the existence-hiding rule, stated on\n Requester.scopes). Enables agents/Brokers to distinguish\n \"resource not in catalog\" from \"resource blocked for your use case\" without\n trial-and-error transactions. Analogous to OpenRTB nbr codes and\n Shutterstock per-item error metadata in batch responses.").optional(), "discovery_method": z.enum(["DISCOVERY_METHOD_EXCHANGE","DISCOVERY_METHOD_SEARCH","DISCOVERY_METHOD_RECOMMENDATION","DISCOVERY_METHOD_SYNDICATION"]).describe("How this URI was discovered by the Broker (v2 extension point).\n v1: always DISCOVERY_METHOD_EXCHANGE (Broker queried an Exchange).\n v2: may include DISCOVERY_METHOD_SEARCH (URI found via search engine like Exa),\n DISCOVERY_METHOD_RECOMMENDATION, etc. The Broker discovers URIs\n through any source, then routes through Exchange for pricing/transaction.\n The discovery method does not affect the transaction flow — it's metadata\n for the agent to understand how the resource was found.").optional(), "offers": z.array(z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"fora.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("ResourceAttestation — Signed envelope of claims from a trusted party.\n\nA provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"fora.attestation.claims_schema\"].")).describe("Signed attestations about the resource at this URI.\n Attestations provide cryptographic proof of\n resource properties from trusted parties (providers or verification vendors).\n\nThree verification levels determine what is independently verifiable:\n Level 0 (no attestations): Resource may carry identifiers (DOI, IPTC GUID)\n for identification, but nothing is cryptographically verifiable.\n Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): Provider signs own claims with Ed25519 key.\n Agent can independently verify content hash and token count.\n CDN delivery failure + content hash mismatch are auto-disputable.\n Level 2 (third-party attested): Independent verification vendor crawled\n the resource and attested to its properties. Agent trusts the attestation\n (does not re-verify hash). Token count discrepancy is auto-disputable\n when corroborated by CDN response size.\n\n Multiple attestations may be present (e.g., provider self-attestation\n plus a third-party verification). Agents choose which to trust.").optional(), "data_as_of": z.string().datetime({ offset: true }).describe("When the offered data was current. For dynamic resources\n (resource_mutability = DYNAMIC), this is the snapshot timestamp.\n Enables the Broker to evaluate freshness: \"this credit report\n reflects data as of March 18\" or \"this drug database was updated today.\"\n\nNot set for STATIC resources (content doesn't change) or LIVE\n resources (content doesn't exist yet).\n\n The Broker compares this against RequestConstraints.max_data_age\n to filter stale offers. Example: agent requests max_data_age = 7 days,\n Broker drops offers where now() - data_as_of > 7 days.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource will be delivered.").default(0), "exchange": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("REQUIRED. Bare host of the Exchange that issued this offer (e.g.\n \"exchange.example\" or \"exchange.example:8081\"), in the form \"Request\n recipient\" defines in the file header. This is the execute-routing target:\n the agent, or a relaying Broker, sends the ExecuteTransaction call for this\n offer to this Exchange, and a Broker relaying a mixed batch groups the items\n by this value. Because it is an ordinary Offer field it falls inside the\n signed bytes (see `signature` below — the signature covers every field\n except `signature` / `signature_algorithm`), so an intermediary cannot\n redirect the execute call to a different Exchange without invalidating the\n offer, and it is what retires the X-FORA-Exchange-Endpoint transport header.\n It is also the audience statement of an ExecuteTransaction, which is why\n TransactionRequest carries no top-level `exchange`: on receipt, an Exchange\n MUST reject the request unless EVERY item's offer.exchange names its own\n domain. Presence is enforced because an empty value is unroutable — a\n relaying Broker has nothing to group or dial on, and the swap-protection\n above is vacuous when the signed bytes carry no recipient at all."), "expires_at": z.string().datetime({ offset: true }).describe("When this offer expires (ISO 8601).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "iab_categories": z.array(z.string()).describe("IAB Content Taxonomy category codes.\n Enables agents to filter offers by topic (e.g., \"only finance resources\").\n Uses IAB Content Taxonomy 3.1 codes.").optional(), "identity": z.object({ "c2pa_manifest": z.string().describe("C2PA content credentials manifest URI.\n Points to a sidecar or embedded C2PA manifest for this resource.\n C2PA-aware agents MAY follow this URI to validate the full provenance\n chain (creator identity, transformation history, ingredient composition)\n using C2PA libraries (JUMBF/COSE Sign1). C2PA-unaware agents can rely\n on c2pa_status and c2pa-bridged attestation claims instead.\n\nFormats:\n Sidecar: HTTPS URI to a .c2pa manifest file\n Embedded: same URI as canonical_url (manifest is inside the asset)\n Content Credentials Cloud: https://contentcredentials.org/verify?uri=...").optional(), "c2pa_status": z.enum(["C2PA_STATUS_TRUSTED","C2PA_STATUS_VALID","C2PA_STATUS_INVALID","C2PA_STATUS_ABSENT"]).describe("The full C2PA validation details (signer identity, trust list,\n action history, training/mining status) are carried in a\n ResourceAttestation with c2pa.* claims — see fora-c2pa-v1 profile.").optional(), "canonical_url": z.string().describe("Provider's authoritative URL for this resource (rel=\"canonical\").\n Always available. Different per provider for syndicated content.").optional(), "content_hash": z.string().describe("Hash of the content. Interpretation depends on hash_method:\n \"simhash-v1\" → locality-sensitive hash, for fuzzy dedup (Level 1)\n \"sha256\" → exact-match integrity hash (Level 2)\n\nLevel 1 (SimHash): computed by Exchange from extracted text.\n Agent verifies that fetched content is \"substantially similar.\"\n Tolerates dynamic page elements.\n\n Level 2 (SHA-256): computed by provider from deterministic payload.\n Agent verifies exact match. Requires provider to serve consistent\n content (e.g., API endpoint, static HTML, structured JSON).\n Mismatch = dispute. Commands premium pricing.").optional(), "doi": z.string().describe("Digital Object Identifier — persistent, never changes.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().describe("Hash algorithm and verification level.\n Examples: \"simhash-v1\", \"minhash-v1\", \"sha256\", \"sha384\"").optional(), "iptc_guid": z.string().describe("IPTC NewsML-G2 globally unique identifier.\n Present when resource flows through news wire syndication (AP, Reuters).").optional(), "isni": z.string().describe("International Standard Name Identifier for the creator.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Drives hash verification behavior:\n STATIC: content_hash is stable. Agent SHOULD verify delivered content matches.\n DYNAMIC: content changes between offer and fetch (credit reports, drug databases).\n content_hash reflects state at offer generation time. Hash mismatch is\n expected and MUST NOT trigger automatic dispute.\n LIVE: content does not exist at offer time (streaming feeds, live broadcasts).\n content_hash is not applicable. The \"resource\" is the stream endpoint.\n\n Validated across 18 use cases: static content (articles, patents, legislation),\n dynamic data (credit reports, drug interactions, stock snapshots), and live\n streams (MarketData quotes, NPR broadcast, news monitoring feeds)."), "soft_binding": z.string().describe("Soft binding hash — content-derived identifier that survives format\n transcoding (resolution changes, compression, PDF-to-text extraction).\n Extracted from C2PA soft binding assertion when present.\n Enables post-delivery verification when the hard binding hash breaks\n due to legitimate format conversion.\n\nAlgorithm specified in soft_binding_method. Values are algorithm-specific\n (e.g., perceptual hash hex string, watermark identifier).").optional(), "soft_binding_method": z.string().describe("Algorithm used for soft_binding.\n Examples: \"phash-v1\" (perceptual hash), \"c2pa-watermark\" (C2PA invisible\n watermark), \"chromaprint\" (audio fingerprint).").optional() }).describe("Resource identity for cross-exchange deduplication.\n Enables Brokers to recognize the same resource offered by\n different Exchanges and compare pricing.").optional(), "offer_id": z.string().describe("Unique identifier for this offer, assigned by the Exchange.\n Opaque to the caller: not derived from the resource, its URL, or any\n other field, and carries no meaning beyond identifying this offer.\n Two offers for the same resource have different offer_ids.").default(""), "previews": z.array(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("Preview — Lightweight resource preview for offer evaluation.\n\nThe Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no FORA transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")).describe("Lightweight previews for offer evaluation.\n The Exchange holds URLs (50–200 bytes each); the provider's CDN serves\n the actual bytes. Agents fetch previews only when evaluating offers —\n not on every discovery query. Multiple previews at different sizes\n allow agents to pick the cheapest fetch for their evaluation needs.\n\nPer content type:\n Image: watermarked thumbnail (150–450px JPEG)\n Video: short clip (10–30s MP4, watermarked)\n Audio: short clip (15–30s MP3, low-bitrate or watermarked)\n Text: snippet or abstract (first 200 words as text/plain)\n Data: sample records (1–3 rows as application/json)\n Stream: optional frame capture or none (streams are priced by time)\n\n Modeled after Shutterstock (multi-size thumbnail URLs),\n Spotify (preview_url to 30s clip), IIIF (parameterized image URLs),\n and OpenRTB native (img.url + dimensions).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("The offer's price, and its ONLY price. An offer represents a single\n licensing arrangement: each projected LicenseTerm yields its own offer, and\n the Exchange moves that term's price here, so the term inside `terms`\n carries no pricing of its own (the offer.terms.pricing_unset rule). This is\n the price execute charges, the price a Broker ranks and compares across\n Exchanges, and the price a metered purchase is charged at. A resource with\n multiple alternative terms (e.g. dual-licensed) produces multiple separate\n offers, one per term — never one offer with a \"headline\" picked among them.\n\nIt derives from exactly one catalog term: the term's pricing as the\n publisher declared it (ResourceEntry.terms). Each offer derives from one\n term, and the offer carries no second copy, so the offer's price and its\n term's price cannot disagree.\n\n On a metered (PER_UNIT) offer, the rate and the estimate a metered\n purchase is charged at are read from here (see Pricing). The estimate is\n optional. When the publisher states one — on the term's own pricing, or\n once for the resource on ResourceEntry.estimated_quantity — the Exchange\n carries it here as estimated_quantity, the term's own estimate taking\n precedence. When the publisher states none, the offer may carry none, and\n the purchase then charges one unit.").optional(), "reporting": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds). A report that arrives after\n the window closed is refused with USAGE_REPORT_REJECTION_REASON_WINDOW_EXPIRED\n (Connect code failed_precondition).").optional() }).describe("Post-usage reporting requirements for this offer.").optional(), "signature": z.string().describe("REQUIRED. Hex-encoded detached Ed25519 signature over the canonical\n serialization of the ENTIRE Offer — every field, including `pricing`,\n `terms` (the full licensing payload), `expires_at`, and `exchange`. Only\n `signature` and `signature_algorithm` are excluded from the signed bytes.\n `expires_at` is signed so the offer's validity window is\n integrity-protected: a relaying Broker cannot extend (or shorten) the TTL\n of a signed offer to replay it outside the window the Exchange intended.\n\nCANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n UNKNOWN FIELDS. A canonicalizer either OMITS content it has no schema for or\n PRESERVES it, and the rule follows from which:\n\n - OMITTING (e.g. proto-JSON, which emits only schema-defined fields): such a\n canonicalizer CANNOT reproduce the signed bytes of a message carrying\n unknown fields — what it renders silently drops part of what the signer\n covered. It MUST refuse the message rather than emit the reduced bytes,\n and a verifier built on it MUST reject rather than verify over them. The\n refusal binds at EVERY depth: a nested message and each element of a\n repeated or map field carries its own unknown-field set.\n - PRESERVING (a canonicalizer that carries unrecognized members through):\n it reproduces the signed bytes faithfully, so there is nothing to refuse.\n\n Either way an APPENDED field cannot pass: an omitting canonicalizer refuses\n the message, and a preserving one renders the appended member into bytes the\n signer never covered, so the signature fails. Without the refusal the omitting\n case would fail OPEN — an intermediary could add unknown fields to an\n already-signed message and leave its signature verifying, smuggling\n unauthenticated content through a message the recipient treats as verified.\n\n Extensions therefore ride in `ext` / `ext_critical`, which are defined fields\n and inside the signed bytes — never as undeclared field numbers.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.").default(""), "signature_algorithm": z.string().describe("JOSE/JWA algorithm identifier (RFC 8037 §3.1). Always 'EdDSA' for\n Ed25519. Advisory only: this field is cleared before the canonical\n payload is signed, so it is not covered by the signature.").default(""), "subscription_id": z.string().describe("If set, this offer is available under an existing subscription/deal.\n No per-request billing — usage tracked against subscription quota.\n Pricing.rate = \"0\" for subscription offers (zero marginal cost).\n The Broker SHOULD prefer subscription offers when available.").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("SubscriptionQuotaInfo — Proactive quota signaling for subscription access.\n\nAnalogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Subscription quota state, when this offer is under a subscription.\n Enables the agent to see remaining quota before committing.\n Multiple entries when the subscription has independent quotas\n (e.g., access count + spend cap).").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Obligation — A post-use behavioral requirement attached to a LicenseTerm.\n\nExamples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).max(64).describe("Post-use behavioral requirements.\n At most 64, for the reason quotas carries.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. Where it is required depends on the message that\n holds the term, so the requirement is a rule of that message rather than of\n this field:\n - On a catalog term (ResourceEntry.terms) it is REQUIRED regardless of\n semantics — an agent cannot act on a priceless term, so absent Pricing\n is a validation error at ingest (resource_entry.terms.pricing_required).\n model = FREE must be stated explicitly (absent Pricing is not free). A\n REFERENCE_ONLY term states its price here too; its License governs the\n human-readable terms but does not replace the machine-readable price.\n - On the term an offer carries (Offer.terms) it MUST be unset\n (offer.terms.pricing_unset): the offer's price is Offer.pricing, stated\n once.").optional(), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The unit being capped — an open vocabulary axis.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quota — A usage cap the term declares.\n\nA quota states how much a licensee may consume under the term before the\n term expires or must be renegotiated. Like a restriction, it is a declared\n term the parties agree to. Quotas are NOT billing quantities — billing is in\n Pricing.\n\n The metric vocabulary is authored ONLY in the (fora.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).max(64).describe("Usage caps. The agent must not exceed any individual Quota.\n At most 64, the bound every per-message list in this contract carries when\n no rule walks it more than once. It bounds what one term may carry, not the\n work of checking one — a validator walks every element it is handed before\n the cap is reported, so the cost of checking is bounded at the transport.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to. Defined-only: the axis set is\n CLOSED, and a number outside it is refused rather than ignored. A custom\n axis is RESTRICTION_KIND_OTHER, whose meaning rides in permitted/prohibited,\n so a new number was never the extension mechanism — accepting one would\n admit a restriction no consumer can evaluate onto a term whose default is\n BINDING (see advisory below), which fails open on the axis a publisher most\n needs honoured. Closing the axis does NOT bound the cost of the one-per-kind\n rule below, and must not be read as doing so: a number this rule refuses is\n still distinct from every other, so that rule's all() finds no duplicate to\n stop on and walks the list in full anyway. Its cost is bounded by the size\n test the rule itself carries."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restriction — A single constraint on one licensing dimension.\n\nRestrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: they are declared terms the parties agree to when the\n agent accepts the offer. The AGENT self-selects the term whose restrictions it\n can honour and bears compliance. The Exchange never enforces a restriction:\n it is NOT an Exchange-side gate the requester must pass to see or buy a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and by the scopes in the requester's signed\n request — see LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (fora.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — FORA user/organization categories")).max(8).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.\n At most 8, and this list is the one of the three that does NOT carry the\n contract's usual 64: only one restriction per axis is valid, the axis enum\n is defined-only, so four is the longest conformant list and eight leaves\n room for an axis this version does not have. Like the caps on quotas and\n obligations, this one bounds the DOCUMENT — how many restrictions one term\n may carry — and not the work of checking it: a validator walks every element\n it is handed before any cardinality rule is reported, so an over-cap list is\n traversed in full on its way to being refused.\n\nWhat makes this list different is that one rule walks it against ITSELF. The\n one-per-kind rule below is quadratic, so it carries its own size test and\n stays silent above this cap; a conformance guard holds the two numbers equal,\n because a cap raised without the test would leave the lists in between\n unchecked for duplicate axes and accepted. The neighbouring disjointness rule\n on each element is quadratic only in that element's two token lists, both\n capped at 64, so its cost is bounded per restriction and linear across the\n list — it needs no such test.").optional(), "scopes": z.array(z.string()).max(64).describe("Scope-gating: the Exchange presents an offer for this term only when the\n scopes in the requester's signed request (Requester.scopes) cover ALL of\n these scopes (AND-semantics). Empty = public. A subscription term is\n Pricing{model:FREE} + scopes:[\"subscription:...\"].\n\nRequester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n also require a verified delegation that grants these scopes before it\n presents the term. To a requester that presents none, it answers as if the\n term did not exist, as existence hiding requires (see Requester.scopes). An\n offer the Exchange presented is honoured until it expires.\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("LicenseTerm — Universal licensing unit.\n\nOne LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation — with one move: on an offer the\n term's price is Offer.pricing, and the term itself carries none.\n\n Validation rules:\n - Pricing MUST be present on EVERY catalog term (ResourceEntry.terms),\n regardless of semantics — the resource_entry.terms.pricing_required\n rule. Absent Pricing → reject at ingest: an agent cannot act on a term\n with no price. This holds for REFERENCE_ONLY too — its License governs\n the human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - Pricing MUST be unset on the term an offer carries (Offer.terms) — the\n offer.terms.pricing_unset rule. The offer's price is Offer.pricing, and\n each offer derives from exactly one catalog term, so the price is stated\n once and cannot disagree with itself. The requirement is therefore a\n rule of the message that holds the term, not of LicenseTerm.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - A PER_UNIT term MAY state Pricing.estimated_quantity. It is not\n required, on the pushed term or on the offer it projects to — see\n Pricing.estimated_quantity.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).min(1).max(1).describe("The licensing term this offer sells, sourced from the publisher's\n ResourceEntry. EXACTLY ONE, and the bound is enforced rather than asked\n for: an offer IS a single licensing arrangement, so N terms on a resource\n project to N offers on that resource, each selling one of them — see\n `pricing` above. An offer with no term would be an offer projected from\n nothing, which is why the floor is one and not zero. The field is\n `repeated` because it shipped that way in v1 and renumbering a released\n field is not available to us, so the cardinality rides as a validation\n rule instead. `ResourceEntry.terms` is the plural side, bounded at 32: a\n resource carries many terms, an offer sells one of them.\n\nThe distinction is not cosmetic. `pricing` is what a Broker ranks on and\n what execute charges, so a term that is not on its own offer has no price\n a Broker can compare and no offer_id an agent can buy. Fusing several\n terms onto one offer makes every term but the first unsellable — a\n dual-licensed resource sells only under whichever arrangement the\n publisher happened to store first.\n\n The term here carries everything the publisher declared EXCEPT its price:\n its `pricing` MUST be unset (the offer.terms.pricing_unset rule), because\n the offer's price is `pricing` above. Its semantics, license,\n restrictions, quotas, obligations, scopes and part_label are the\n publisher's, unchanged.\n\n Where a term is reachable only under an existing subscription, its offer\n carries `subscription_id` and prices at zero marginal cost; the terms a\n requester may see at all are selected by `LicenseTerm.scopes`.\n\n See: Universal Licensing Core section.").optional(), "title": z.string().describe("Resource title (human-readable, for display/logging).").optional() }).describe("Offer — A single resource offer from an Exchange.\n\nCombines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via fora-comp-v1 extension profile.")).describe("Zero or more offers for this URI. Empty = resource not available.").optional(), "restriction_filters": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("When absence_reason = RESTRICTION_FILTERED, the restriction axes that drove\n the convenience pre-filter, in the same RestrictionKind vocabulary the terms\n use (e.g. [GEOGRAPHY] when the requester's stated geography matched no term).\n Advisory diagnostics, not an enforcement verdict.").optional(), "uri": z.string().describe("The URI this group of offers is for (echoed from ResourceQuery.uris).").default("") }).describe("OfferGroup — Offers for a single requested URI.\n Enables multi-URI batch queries where the caller needs to know\n which offers correspond to which requested resource.")).describe("Offers grouped by requested URI (for multi-URI batch queries).\n When populated, `offers` SHOULD be empty to avoid ambiguity.").optional(), "offers": z.array(z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"fora.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("ResourceAttestation — Signed envelope of claims from a trusted party.\n\nA provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"fora.attestation.claims_schema\"].")).describe("Signed attestations about the resource at this URI.\n Attestations provide cryptographic proof of\n resource properties from trusted parties (providers or verification vendors).\n\nThree verification levels determine what is independently verifiable:\n Level 0 (no attestations): Resource may carry identifiers (DOI, IPTC GUID)\n for identification, but nothing is cryptographically verifiable.\n Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): Provider signs own claims with Ed25519 key.\n Agent can independently verify content hash and token count.\n CDN delivery failure + content hash mismatch are auto-disputable.\n Level 2 (third-party attested): Independent verification vendor crawled\n the resource and attested to its properties. Agent trusts the attestation\n (does not re-verify hash). Token count discrepancy is auto-disputable\n when corroborated by CDN response size.\n\n Multiple attestations may be present (e.g., provider self-attestation\n plus a third-party verification). Agents choose which to trust.").optional(), "data_as_of": z.string().datetime({ offset: true }).describe("When the offered data was current. For dynamic resources\n (resource_mutability = DYNAMIC), this is the snapshot timestamp.\n Enables the Broker to evaluate freshness: \"this credit report\n reflects data as of March 18\" or \"this drug database was updated today.\"\n\nNot set for STATIC resources (content doesn't change) or LIVE\n resources (content doesn't exist yet).\n\n The Broker compares this against RequestConstraints.max_data_age\n to filter stale offers. Example: agent requests max_data_age = 7 days,\n Broker drops offers where now() - data_as_of > 7 days.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource will be delivered.").default(0), "exchange": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("REQUIRED. Bare host of the Exchange that issued this offer (e.g.\n \"exchange.example\" or \"exchange.example:8081\"), in the form \"Request\n recipient\" defines in the file header. This is the execute-routing target:\n the agent, or a relaying Broker, sends the ExecuteTransaction call for this\n offer to this Exchange, and a Broker relaying a mixed batch groups the items\n by this value. Because it is an ordinary Offer field it falls inside the\n signed bytes (see `signature` below — the signature covers every field\n except `signature` / `signature_algorithm`), so an intermediary cannot\n redirect the execute call to a different Exchange without invalidating the\n offer, and it is what retires the X-FORA-Exchange-Endpoint transport header.\n It is also the audience statement of an ExecuteTransaction, which is why\n TransactionRequest carries no top-level `exchange`: on receipt, an Exchange\n MUST reject the request unless EVERY item's offer.exchange names its own\n domain. Presence is enforced because an empty value is unroutable — a\n relaying Broker has nothing to group or dial on, and the swap-protection\n above is vacuous when the signed bytes carry no recipient at all."), "expires_at": z.string().datetime({ offset: true }).describe("When this offer expires (ISO 8601).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "iab_categories": z.array(z.string()).describe("IAB Content Taxonomy category codes.\n Enables agents to filter offers by topic (e.g., \"only finance resources\").\n Uses IAB Content Taxonomy 3.1 codes.").optional(), "identity": z.object({ "c2pa_manifest": z.string().describe("C2PA content credentials manifest URI.\n Points to a sidecar or embedded C2PA manifest for this resource.\n C2PA-aware agents MAY follow this URI to validate the full provenance\n chain (creator identity, transformation history, ingredient composition)\n using C2PA libraries (JUMBF/COSE Sign1). C2PA-unaware agents can rely\n on c2pa_status and c2pa-bridged attestation claims instead.\n\nFormats:\n Sidecar: HTTPS URI to a .c2pa manifest file\n Embedded: same URI as canonical_url (manifest is inside the asset)\n Content Credentials Cloud: https://contentcredentials.org/verify?uri=...").optional(), "c2pa_status": z.enum(["C2PA_STATUS_TRUSTED","C2PA_STATUS_VALID","C2PA_STATUS_INVALID","C2PA_STATUS_ABSENT"]).describe("The full C2PA validation details (signer identity, trust list,\n action history, training/mining status) are carried in a\n ResourceAttestation with c2pa.* claims — see fora-c2pa-v1 profile.").optional(), "canonical_url": z.string().describe("Provider's authoritative URL for this resource (rel=\"canonical\").\n Always available. Different per provider for syndicated content.").optional(), "content_hash": z.string().describe("Hash of the content. Interpretation depends on hash_method:\n \"simhash-v1\" → locality-sensitive hash, for fuzzy dedup (Level 1)\n \"sha256\" → exact-match integrity hash (Level 2)\n\nLevel 1 (SimHash): computed by Exchange from extracted text.\n Agent verifies that fetched content is \"substantially similar.\"\n Tolerates dynamic page elements.\n\n Level 2 (SHA-256): computed by provider from deterministic payload.\n Agent verifies exact match. Requires provider to serve consistent\n content (e.g., API endpoint, static HTML, structured JSON).\n Mismatch = dispute. Commands premium pricing.").optional(), "doi": z.string().describe("Digital Object Identifier — persistent, never changes.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().describe("Hash algorithm and verification level.\n Examples: \"simhash-v1\", \"minhash-v1\", \"sha256\", \"sha384\"").optional(), "iptc_guid": z.string().describe("IPTC NewsML-G2 globally unique identifier.\n Present when resource flows through news wire syndication (AP, Reuters).").optional(), "isni": z.string().describe("International Standard Name Identifier for the creator.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Drives hash verification behavior:\n STATIC: content_hash is stable. Agent SHOULD verify delivered content matches.\n DYNAMIC: content changes between offer and fetch (credit reports, drug databases).\n content_hash reflects state at offer generation time. Hash mismatch is\n expected and MUST NOT trigger automatic dispute.\n LIVE: content does not exist at offer time (streaming feeds, live broadcasts).\n content_hash is not applicable. The \"resource\" is the stream endpoint.\n\n Validated across 18 use cases: static content (articles, patents, legislation),\n dynamic data (credit reports, drug interactions, stock snapshots), and live\n streams (MarketData quotes, NPR broadcast, news monitoring feeds)."), "soft_binding": z.string().describe("Soft binding hash — content-derived identifier that survives format\n transcoding (resolution changes, compression, PDF-to-text extraction).\n Extracted from C2PA soft binding assertion when present.\n Enables post-delivery verification when the hard binding hash breaks\n due to legitimate format conversion.\n\nAlgorithm specified in soft_binding_method. Values are algorithm-specific\n (e.g., perceptual hash hex string, watermark identifier).").optional(), "soft_binding_method": z.string().describe("Algorithm used for soft_binding.\n Examples: \"phash-v1\" (perceptual hash), \"c2pa-watermark\" (C2PA invisible\n watermark), \"chromaprint\" (audio fingerprint).").optional() }).describe("Resource identity for cross-exchange deduplication.\n Enables Brokers to recognize the same resource offered by\n different Exchanges and compare pricing.").optional(), "offer_id": z.string().describe("Unique identifier for this offer, assigned by the Exchange.\n Opaque to the caller: not derived from the resource, its URL, or any\n other field, and carries no meaning beyond identifying this offer.\n Two offers for the same resource have different offer_ids.").default(""), "previews": z.array(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("Preview — Lightweight resource preview for offer evaluation.\n\nThe Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no FORA transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")).describe("Lightweight previews for offer evaluation.\n The Exchange holds URLs (50–200 bytes each); the provider's CDN serves\n the actual bytes. Agents fetch previews only when evaluating offers —\n not on every discovery query. Multiple previews at different sizes\n allow agents to pick the cheapest fetch for their evaluation needs.\n\nPer content type:\n Image: watermarked thumbnail (150–450px JPEG)\n Video: short clip (10–30s MP4, watermarked)\n Audio: short clip (15–30s MP3, low-bitrate or watermarked)\n Text: snippet or abstract (first 200 words as text/plain)\n Data: sample records (1–3 rows as application/json)\n Stream: optional frame capture or none (streams are priced by time)\n\n Modeled after Shutterstock (multi-size thumbnail URLs),\n Spotify (preview_url to 30s clip), IIIF (parameterized image URLs),\n and OpenRTB native (img.url + dimensions).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("The offer's price, and its ONLY price. An offer represents a single\n licensing arrangement: each projected LicenseTerm yields its own offer, and\n the Exchange moves that term's price here, so the term inside `terms`\n carries no pricing of its own (the offer.terms.pricing_unset rule). This is\n the price execute charges, the price a Broker ranks and compares across\n Exchanges, and the price a metered purchase is charged at. A resource with\n multiple alternative terms (e.g. dual-licensed) produces multiple separate\n offers, one per term — never one offer with a \"headline\" picked among them.\n\nIt derives from exactly one catalog term: the term's pricing as the\n publisher declared it (ResourceEntry.terms). Each offer derives from one\n term, and the offer carries no second copy, so the offer's price and its\n term's price cannot disagree.\n\n On a metered (PER_UNIT) offer, the rate and the estimate a metered\n purchase is charged at are read from here (see Pricing). The estimate is\n optional. When the publisher states one — on the term's own pricing, or\n once for the resource on ResourceEntry.estimated_quantity — the Exchange\n carries it here as estimated_quantity, the term's own estimate taking\n precedence. When the publisher states none, the offer may carry none, and\n the purchase then charges one unit.").optional(), "reporting": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds). A report that arrives after\n the window closed is refused with USAGE_REPORT_REJECTION_REASON_WINDOW_EXPIRED\n (Connect code failed_precondition).").optional() }).describe("Post-usage reporting requirements for this offer.").optional(), "signature": z.string().describe("REQUIRED. Hex-encoded detached Ed25519 signature over the canonical\n serialization of the ENTIRE Offer — every field, including `pricing`,\n `terms` (the full licensing payload), `expires_at`, and `exchange`. Only\n `signature` and `signature_algorithm` are excluded from the signed bytes.\n `expires_at` is signed so the offer's validity window is\n integrity-protected: a relaying Broker cannot extend (or shorten) the TTL\n of a signed offer to replay it outside the window the Exchange intended.\n\nCANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n UNKNOWN FIELDS. A canonicalizer either OMITS content it has no schema for or\n PRESERVES it, and the rule follows from which:\n\n - OMITTING (e.g. proto-JSON, which emits only schema-defined fields): such a\n canonicalizer CANNOT reproduce the signed bytes of a message carrying\n unknown fields — what it renders silently drops part of what the signer\n covered. It MUST refuse the message rather than emit the reduced bytes,\n and a verifier built on it MUST reject rather than verify over them. The\n refusal binds at EVERY depth: a nested message and each element of a\n repeated or map field carries its own unknown-field set.\n - PRESERVING (a canonicalizer that carries unrecognized members through):\n it reproduces the signed bytes faithfully, so there is nothing to refuse.\n\n Either way an APPENDED field cannot pass: an omitting canonicalizer refuses\n the message, and a preserving one renders the appended member into bytes the\n signer never covered, so the signature fails. Without the refusal the omitting\n case would fail OPEN — an intermediary could add unknown fields to an\n already-signed message and leave its signature verifying, smuggling\n unauthenticated content through a message the recipient treats as verified.\n\n Extensions therefore ride in `ext` / `ext_critical`, which are defined fields\n and inside the signed bytes — never as undeclared field numbers.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.").default(""), "signature_algorithm": z.string().describe("JOSE/JWA algorithm identifier (RFC 8037 §3.1). Always 'EdDSA' for\n Ed25519. Advisory only: this field is cleared before the canonical\n payload is signed, so it is not covered by the signature.").default(""), "subscription_id": z.string().describe("If set, this offer is available under an existing subscription/deal.\n No per-request billing — usage tracked against subscription quota.\n Pricing.rate = \"0\" for subscription offers (zero marginal cost).\n The Broker SHOULD prefer subscription offers when available.").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("SubscriptionQuotaInfo — Proactive quota signaling for subscription access.\n\nAnalogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Subscription quota state, when this offer is under a subscription.\n Enables the agent to see remaining quota before committing.\n Multiple entries when the subscription has independent quotas\n (e.g., access count + spend cap).").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Obligation — A post-use behavioral requirement attached to a LicenseTerm.\n\nExamples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).max(64).describe("Post-use behavioral requirements.\n At most 64, for the reason quotas carries.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. Where it is required depends on the message that\n holds the term, so the requirement is a rule of that message rather than of\n this field:\n - On a catalog term (ResourceEntry.terms) it is REQUIRED regardless of\n semantics — an agent cannot act on a priceless term, so absent Pricing\n is a validation error at ingest (resource_entry.terms.pricing_required).\n model = FREE must be stated explicitly (absent Pricing is not free). A\n REFERENCE_ONLY term states its price here too; its License governs the\n human-readable terms but does not replace the machine-readable price.\n - On the term an offer carries (Offer.terms) it MUST be unset\n (offer.terms.pricing_unset): the offer's price is Offer.pricing, stated\n once.").optional(), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The unit being capped — an open vocabulary axis.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quota — A usage cap the term declares.\n\nA quota states how much a licensee may consume under the term before the\n term expires or must be renegotiated. Like a restriction, it is a declared\n term the parties agree to. Quotas are NOT billing quantities — billing is in\n Pricing.\n\n The metric vocabulary is authored ONLY in the (fora.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).max(64).describe("Usage caps. The agent must not exceed any individual Quota.\n At most 64, the bound every per-message list in this contract carries when\n no rule walks it more than once. It bounds what one term may carry, not the\n work of checking one — a validator walks every element it is handed before\n the cap is reported, so the cost of checking is bounded at the transport.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to. Defined-only: the axis set is\n CLOSED, and a number outside it is refused rather than ignored. A custom\n axis is RESTRICTION_KIND_OTHER, whose meaning rides in permitted/prohibited,\n so a new number was never the extension mechanism — accepting one would\n admit a restriction no consumer can evaluate onto a term whose default is\n BINDING (see advisory below), which fails open on the axis a publisher most\n needs honoured. Closing the axis does NOT bound the cost of the one-per-kind\n rule below, and must not be read as doing so: a number this rule refuses is\n still distinct from every other, so that rule's all() finds no duplicate to\n stop on and walks the list in full anyway. Its cost is bounded by the size\n test the rule itself carries."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restriction — A single constraint on one licensing dimension.\n\nRestrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: they are declared terms the parties agree to when the\n agent accepts the offer. The AGENT self-selects the term whose restrictions it\n can honour and bears compliance. The Exchange never enforces a restriction:\n it is NOT an Exchange-side gate the requester must pass to see or buy a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and by the scopes in the requester's signed\n request — see LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (fora.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — FORA user/organization categories")).max(8).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.\n At most 8, and this list is the one of the three that does NOT carry the\n contract's usual 64: only one restriction per axis is valid, the axis enum\n is defined-only, so four is the longest conformant list and eight leaves\n room for an axis this version does not have. Like the caps on quotas and\n obligations, this one bounds the DOCUMENT — how many restrictions one term\n may carry — and not the work of checking it: a validator walks every element\n it is handed before any cardinality rule is reported, so an over-cap list is\n traversed in full on its way to being refused.\n\nWhat makes this list different is that one rule walks it against ITSELF. The\n one-per-kind rule below is quadratic, so it carries its own size test and\n stays silent above this cap; a conformance guard holds the two numbers equal,\n because a cap raised without the test would leave the lists in between\n unchecked for duplicate axes and accepted. The neighbouring disjointness rule\n on each element is quadratic only in that element's two token lists, both\n capped at 64, so its cost is bounded per restriction and linear across the\n list — it needs no such test.").optional(), "scopes": z.array(z.string()).max(64).describe("Scope-gating: the Exchange presents an offer for this term only when the\n scopes in the requester's signed request (Requester.scopes) cover ALL of\n these scopes (AND-semantics). Empty = public. A subscription term is\n Pricing{model:FREE} + scopes:[\"subscription:...\"].\n\nRequester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n also require a verified delegation that grants these scopes before it\n presents the term. To a requester that presents none, it answers as if the\n term did not exist, as existence hiding requires (see Requester.scopes). An\n offer the Exchange presented is honoured until it expires.\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("LicenseTerm — Universal licensing unit.\n\nOne LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation — with one move: on an offer the\n term's price is Offer.pricing, and the term itself carries none.\n\n Validation rules:\n - Pricing MUST be present on EVERY catalog term (ResourceEntry.terms),\n regardless of semantics — the resource_entry.terms.pricing_required\n rule. Absent Pricing → reject at ingest: an agent cannot act on a term\n with no price. This holds for REFERENCE_ONLY too — its License governs\n the human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - Pricing MUST be unset on the term an offer carries (Offer.terms) — the\n offer.terms.pricing_unset rule. The offer's price is Offer.pricing, and\n each offer derives from exactly one catalog term, so the price is stated\n once and cannot disagree with itself. The requirement is therefore a\n rule of the message that holds the term, not of LicenseTerm.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - A PER_UNIT term MAY state Pricing.estimated_quantity. It is not\n required, on the pushed term or on the offer it projects to — see\n Pricing.estimated_quantity.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).min(1).max(1).describe("The licensing term this offer sells, sourced from the publisher's\n ResourceEntry. EXACTLY ONE, and the bound is enforced rather than asked\n for: an offer IS a single licensing arrangement, so N terms on a resource\n project to N offers on that resource, each selling one of them — see\n `pricing` above. An offer with no term would be an offer projected from\n nothing, which is why the floor is one and not zero. The field is\n `repeated` because it shipped that way in v1 and renumbering a released\n field is not available to us, so the cardinality rides as a validation\n rule instead. `ResourceEntry.terms` is the plural side, bounded at 32: a\n resource carries many terms, an offer sells one of them.\n\nThe distinction is not cosmetic. `pricing` is what a Broker ranks on and\n what execute charges, so a term that is not on its own offer has no price\n a Broker can compare and no offer_id an agent can buy. Fusing several\n terms onto one offer makes every term but the first unsellable — a\n dual-licensed resource sells only under whichever arrangement the\n publisher happened to store first.\n\n The term here carries everything the publisher declared EXCEPT its price:\n its `pricing` MUST be unset (the offer.terms.pricing_unset rule), because\n the offer's price is `pricing` above. Its semantics, license,\n restrictions, quotas, obligations, scopes and part_label are the\n publisher's, unchanged.\n\n Where a term is reachable only under an existing subscription, its offer\n carries `subscription_id` and prices at zero marginal cost; the terms a\n requester may see at all are selected by `LicenseTerm.scopes`.\n\n See: Universal Licensing Core section.").optional(), "title": z.string().describe("Resource title (human-readable, for display/logging).").optional() }).describe("Offer — A single resource offer from an Exchange.\n\nCombines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via fora-comp-v1 extension profile.")).describe("Flat list of offers (for single-URI queries).").optional(), "rate_limit": z.object({ "limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Maximum requests allowed in the current window.").optional(), "remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Requests remaining in the current window.").optional(), "reset_at": z.string().datetime({ offset: true }).describe("When the current window resets (UTC). After this time, `remaining` resets to `limit`.").optional(), "window": z.string().describe("Duration of the rate limit window (e.g. 60s = per-minute limit).").optional() }).describe("Rate limit status for this caller.\n Present when the Exchange enforces per-caller rate limits on discovery.\n Enables agents/Brokers to throttle proactively rather than hitting\n hard limits. Particularly important when a Broker fans out the\n same batch query to multiple Exchanges — mid-batch rate limiting\n can cause partial results if not signaled early.").optional(), "ver": z.string().describe("FORA protocol version — \"1.0\". Stamped by the sender from a single\n constant; advisory on receive. See \"Protocol version\" in the file header.").default("") }).describe("ResourceResponse — Exchange returns candidate resource offers.\n\nWhen the ResourceQuery contains multiple URIs, offers are grouped by URI\n via OfferGroup. When a single URI is queried, the Exchange MAY use\n either the flat `offers` field or a single OfferGroup.")))(); +export const ResourceResponseSchema = /* @__PURE__ */ (() => wire(z.object({ "exchange": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("Canonical domain of the responding Exchange, in the shape \"Request\n recipient\" defines in the file header. The response counterpart of the\n recipient field on the request: it names who answered."), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "offer_groups": z.array(z.object({ "absence_reason": z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"]).describe("Why no offers are available for this URI.\n Set only when `offers` is empty, and not always then: a resource the\n requester's scopes leave with no presentable offer is answered with empty\n `offers` and this field unset (the existence-hiding rule, stated on\n Requester.scopes). Enables agents/Brokers to distinguish\n \"resource not in catalog\" from \"resource blocked for your use case\" without\n trial-and-error transactions. Analogous to OpenRTB nbr codes and\n Shutterstock per-item error metadata in batch responses.").optional(), "discovery_method": z.enum(["DISCOVERY_METHOD_EXCHANGE","DISCOVERY_METHOD_SEARCH","DISCOVERY_METHOD_RECOMMENDATION","DISCOVERY_METHOD_SYNDICATION"]).describe("How this URI was discovered by the Broker (v2 extension point).\n v1: always DISCOVERY_METHOD_EXCHANGE (Broker queried an Exchange).\n v2: may include DISCOVERY_METHOD_SEARCH (URI found via search engine like Exa),\n DISCOVERY_METHOD_RECOMMENDATION, etc. The Broker discovers URIs\n through any source, then routes through Exchange for pricing/transaction.\n The discovery method does not affect the transaction flow — it's metadata\n for the agent to understand how the resource was found.").optional(), "offers": z.array(z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"fora.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("ResourceAttestation — Signed envelope of claims from a trusted party.\n\nA provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"fora.attestation.claims_schema\"].")).describe("Signed attestations about the resource at this URI.\n Attestations provide cryptographic proof of\n resource properties from trusted parties (providers or verification vendors).\n\nThree verification levels determine what is independently verifiable:\n Level 0 (no attestations): Resource may carry identifiers (DOI, IPTC GUID)\n for identification, but nothing is cryptographically verifiable.\n Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): Provider signs own claims with Ed25519 key.\n Agent can independently verify content hash and token count.\n CDN delivery failure + content hash mismatch are auto-disputable.\n Level 2 (third-party attested): Independent verification vendor crawled\n the resource and attested to its properties. Agent trusts the attestation\n (does not re-verify hash). Token count discrepancy is auto-disputable\n when corroborated by CDN response size.\n\n Multiple attestations may be present (e.g., provider self-attestation\n plus a third-party verification). Agents choose which to trust.").optional(), "data_as_of": z.string().datetime({ offset: true }).describe("When the offered data was current. For dynamic resources\n (resource_mutability = DYNAMIC), this is the snapshot timestamp.\n Enables the Broker to evaluate freshness: \"this credit report\n reflects data as of March 18\" or \"this drug database was updated today.\"\n\nNot set for STATIC resources (content doesn't change) or LIVE\n resources (content doesn't exist yet).\n\n The Broker compares this against RequestConstraints.max_data_age\n to filter stale offers. Example: agent requests max_data_age = 7 days,\n Broker drops offers where now() - data_as_of > 7 days.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource will be delivered.").default(0), "exchange": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("REQUIRED. Bare host of the Exchange that issued this offer (e.g.\n \"exchange.example\" or \"exchange.example:8081\"), in the form \"Request\n recipient\" defines in the file header. This is the execute-routing target:\n the agent, or a relaying Broker, sends the ExecuteTransaction call for this\n offer to this Exchange, and a Broker relaying a mixed batch groups the items\n by this value. Because it is an ordinary Offer field it falls inside the\n signed bytes (see `signature` below — the signature covers every field\n except `signature` / `signature_algorithm`), so an intermediary cannot\n redirect the execute call to a different Exchange without invalidating the\n offer, and it is what retires the X-FORA-Exchange-Endpoint transport header.\n It is also the audience statement of an ExecuteTransaction, which is why\n TransactionRequest carries no top-level `exchange`: on receipt, an Exchange\n MUST reject the request unless EVERY item's offer.exchange names its own\n domain. Presence is enforced because an empty value is unroutable — a\n relaying Broker has nothing to group or dial on, and the swap-protection\n above is vacuous when the signed bytes carry no recipient at all."), "expires_at": z.string().datetime({ offset: true }).describe("When this offer expires (ISO 8601). An offer for a term gated behind a\n verified delegation MUST NOT expire later than that delegation chain's\n effective expiry, the earliest `exp` in the chain (see Delegation).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "iab_categories": z.array(z.string()).describe("IAB Content Taxonomy category codes.\n Enables agents to filter offers by topic (e.g., \"only finance resources\").\n Uses IAB Content Taxonomy 3.1 codes.").optional(), "identity": z.object({ "c2pa_manifest": z.string().describe("C2PA content credentials manifest URI.\n Points to a sidecar or embedded C2PA manifest for this resource.\n C2PA-aware agents MAY follow this URI to validate the full provenance\n chain (creator identity, transformation history, ingredient composition)\n using C2PA libraries (JUMBF/COSE Sign1). C2PA-unaware agents can rely\n on c2pa_status and c2pa-bridged attestation claims instead.\n\nFormats:\n Sidecar: HTTPS URI to a .c2pa manifest file\n Embedded: same URI as canonical_url (manifest is inside the asset)\n Content Credentials Cloud: https://contentcredentials.org/verify?uri=...").optional(), "c2pa_status": z.enum(["C2PA_STATUS_TRUSTED","C2PA_STATUS_VALID","C2PA_STATUS_INVALID","C2PA_STATUS_ABSENT"]).describe("The full C2PA validation details (signer identity, trust list,\n action history, training/mining status) are carried in a\n ResourceAttestation with c2pa.* claims — see fora-c2pa-v1 profile.").optional(), "canonical_url": z.string().describe("Provider's authoritative URL for this resource (rel=\"canonical\").\n Always available. Different per provider for syndicated content.").optional(), "content_hash": z.string().describe("Hash of the content. Interpretation depends on hash_method:\n \"simhash-v1\" → locality-sensitive hash, for fuzzy dedup (Level 1)\n \"sha256\" → exact-match integrity hash (Level 2)\n\nLevel 1 (SimHash): computed by Exchange from extracted text.\n Agent verifies that fetched content is \"substantially similar.\"\n Tolerates dynamic page elements.\n\n Level 2 (SHA-256): computed by provider from deterministic payload.\n Agent verifies exact match. Requires provider to serve consistent\n content (e.g., API endpoint, static HTML, structured JSON).\n Mismatch = dispute. Commands premium pricing.").optional(), "doi": z.string().describe("Digital Object Identifier — persistent, never changes.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().describe("Hash algorithm and verification level.\n Examples: \"simhash-v1\", \"minhash-v1\", \"sha256\", \"sha384\"").optional(), "iptc_guid": z.string().describe("IPTC NewsML-G2 globally unique identifier.\n Present when resource flows through news wire syndication (AP, Reuters).").optional(), "isni": z.string().describe("International Standard Name Identifier for the creator.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Drives hash verification behavior:\n STATIC: content_hash is stable. Agent SHOULD verify delivered content matches.\n DYNAMIC: content changes between offer and fetch (credit reports, drug databases).\n content_hash reflects state at offer generation time. Hash mismatch is\n expected and MUST NOT trigger automatic dispute.\n LIVE: content does not exist at offer time (streaming feeds, live broadcasts).\n content_hash is not applicable. The \"resource\" is the stream endpoint.\n\n Validated across 18 use cases: static content (articles, patents, legislation),\n dynamic data (credit reports, drug interactions, stock snapshots), and live\n streams (MarketData quotes, NPR broadcast, news monitoring feeds)."), "soft_binding": z.string().describe("Soft binding hash — content-derived identifier that survives format\n transcoding (resolution changes, compression, PDF-to-text extraction).\n Extracted from C2PA soft binding assertion when present.\n Enables post-delivery verification when the hard binding hash breaks\n due to legitimate format conversion.\n\nAlgorithm specified in soft_binding_method. Values are algorithm-specific\n (e.g., perceptual hash hex string, watermark identifier).").optional(), "soft_binding_method": z.string().describe("Algorithm used for soft_binding.\n Examples: \"phash-v1\" (perceptual hash), \"c2pa-watermark\" (C2PA invisible\n watermark), \"chromaprint\" (audio fingerprint).").optional() }).describe("Resource identity for cross-exchange deduplication.\n Enables Brokers to recognize the same resource offered by\n different Exchanges and compare pricing.").optional(), "offer_id": z.string().describe("Unique identifier for this offer, assigned by the Exchange.\n Opaque to the caller: not derived from the resource, its URL, or any\n other field, and carries no meaning beyond identifying this offer.\n Two offers for the same resource have different offer_ids.").default(""), "previews": z.array(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("Preview — Lightweight resource preview for offer evaluation.\n\nThe Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no FORA transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")).describe("Lightweight previews for offer evaluation.\n The Exchange holds URLs (50–200 bytes each); the provider's CDN serves\n the actual bytes. Agents fetch previews only when evaluating offers —\n not on every discovery query. Multiple previews at different sizes\n allow agents to pick the cheapest fetch for their evaluation needs.\n\nPer content type:\n Image: watermarked thumbnail (150–450px JPEG)\n Video: short clip (10–30s MP4, watermarked)\n Audio: short clip (15–30s MP3, low-bitrate or watermarked)\n Text: snippet or abstract (first 200 words as text/plain)\n Data: sample records (1–3 rows as application/json)\n Stream: optional frame capture or none (streams are priced by time)\n\n Modeled after Shutterstock (multi-size thumbnail URLs),\n Spotify (preview_url to 30s clip), IIIF (parameterized image URLs),\n and OpenRTB native (img.url + dimensions).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("The offer's price, and its ONLY price. An offer represents a single\n licensing arrangement: each projected LicenseTerm yields its own offer, and\n the Exchange moves that term's price here, so the term inside `terms`\n carries no pricing of its own (the offer.terms.pricing_unset rule). This is\n the price execute charges, the price a Broker ranks and compares across\n Exchanges, and the price a metered purchase is charged at. A resource with\n multiple alternative terms (e.g. dual-licensed) produces multiple separate\n offers, one per term — never one offer with a \"headline\" picked among them.\n\nIt derives from exactly one catalog term: the term's pricing as the\n publisher declared it (ResourceEntry.terms). Each offer derives from one\n term, and the offer carries no second copy, so the offer's price and its\n term's price cannot disagree.\n\n On a metered (PER_UNIT) offer, the rate and the estimate a metered\n purchase is charged at are read from here (see Pricing). The estimate is\n optional. When the publisher states one — on the term's own pricing, or\n once for the resource on ResourceEntry.estimated_quantity — the Exchange\n carries it here as estimated_quantity, the term's own estimate taking\n precedence. When the publisher states none, the offer may carry none, and\n the purchase then charges one unit.").optional(), "reporting": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds). A report that arrives after\n the window closed is refused with USAGE_REPORT_REJECTION_REASON_WINDOW_EXPIRED\n (Connect code failed_precondition).").optional() }).describe("Post-usage reporting requirements for this offer.").optional(), "signature": z.string().describe("REQUIRED. Hex-encoded detached Ed25519 signature over the canonical\n serialization of the ENTIRE Offer — every field, including `pricing`,\n `terms` (the full licensing payload), `expires_at`, and `exchange`. Only\n `signature` and `signature_algorithm` are excluded from the signed bytes.\n `expires_at` is signed so the offer's validity window is\n integrity-protected: a relaying Broker cannot extend (or shorten) the TTL\n of a signed offer to replay it outside the window the Exchange intended.\n\nCANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n UNKNOWN FIELDS. A canonicalizer either OMITS content it has no schema for or\n PRESERVES it, and the rule follows from which:\n\n - OMITTING (e.g. proto-JSON, which emits only schema-defined fields): such a\n canonicalizer CANNOT reproduce the signed bytes of a message carrying\n unknown fields — what it renders silently drops part of what the signer\n covered. It MUST refuse the message rather than emit the reduced bytes,\n and a verifier built on it MUST reject rather than verify over them. The\n refusal binds at EVERY depth: a nested message and each element of a\n repeated or map field carries its own unknown-field set.\n - PRESERVING (a canonicalizer that carries unrecognized members through):\n it reproduces the signed bytes faithfully, so there is nothing to refuse.\n\n Either way an APPENDED field cannot pass: an omitting canonicalizer refuses\n the message, and a preserving one renders the appended member into bytes the\n signer never covered, so the signature fails. Without the refusal the omitting\n case would fail OPEN — an intermediary could add unknown fields to an\n already-signed message and leave its signature verifying, smuggling\n unauthenticated content through a message the recipient treats as verified.\n\n Extensions therefore ride in `ext` / `ext_critical`, which are defined fields\n and inside the signed bytes — never as undeclared field numbers.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.").default(""), "signature_algorithm": z.string().describe("JOSE/JWA algorithm identifier (RFC 8037 §3.1). Always 'EdDSA' for\n Ed25519. Advisory only: this field is cleared before the canonical\n payload is signed, so it is not covered by the signature.").default(""), "subscription_id": z.string().describe("If set, this offer is available under an existing subscription/deal.\n No per-request billing — usage tracked against subscription quota.\n Pricing.rate = \"0\" for subscription offers (zero marginal cost).\n The Broker SHOULD prefer subscription offers when available.").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("SubscriptionQuotaInfo — Proactive quota signaling for subscription access.\n\nAnalogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Subscription quota state, when this offer is under a subscription.\n Enables the agent to see remaining quota before committing.\n Multiple entries when the subscription has independent quotas\n (e.g., access count + spend cap).").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Obligation — A post-use behavioral requirement attached to a LicenseTerm.\n\nExamples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).max(64).describe("Post-use behavioral requirements.\n At most 64, for the reason quotas carries.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. Where it is required depends on the message that\n holds the term, so the requirement is a rule of that message rather than of\n this field:\n - On a catalog term (ResourceEntry.terms) it is REQUIRED regardless of\n semantics — an agent cannot act on a priceless term, so absent Pricing\n is a validation error at ingest (resource_entry.terms.pricing_required).\n model = FREE must be stated explicitly (absent Pricing is not free). A\n REFERENCE_ONLY term states its price here too; its License governs the\n human-readable terms but does not replace the machine-readable price.\n - On the term an offer carries (Offer.terms) it MUST be unset\n (offer.terms.pricing_unset): the offer's price is Offer.pricing, stated\n once.").optional(), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The unit being capped — an open vocabulary axis.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quota — A usage cap the term declares.\n\nA quota states how much a licensee may consume under the term before the\n term expires or must be renegotiated. Like a restriction, it is a declared\n term the parties agree to. Quotas are NOT billing quantities — billing is in\n Pricing.\n\n The metric vocabulary is authored ONLY in the (fora.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).max(64).describe("Usage caps. The agent must not exceed any individual Quota.\n At most 64, the bound every per-message list in this contract carries when\n no rule walks it more than once. It bounds what one term may carry, not the\n work of checking one — a validator walks every element it is handed before\n the cap is reported, so the cost of checking is bounded at the transport.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to. Defined-only: the axis set is\n CLOSED, and a number outside it is refused rather than ignored. A custom\n axis is RESTRICTION_KIND_OTHER, whose meaning rides in permitted/prohibited,\n so a new number was never the extension mechanism — accepting one would\n admit a restriction no consumer can evaluate onto a term whose default is\n BINDING (see advisory below), which fails open on the axis a publisher most\n needs honoured. Closing the axis does NOT bound the cost of the one-per-kind\n rule below, and must not be read as doing so: a number this rule refuses is\n still distinct from every other, so that rule's all() finds no duplicate to\n stop on and walks the list in full anyway. Its cost is bounded by the size\n test the rule itself carries."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restriction — A single constraint on one licensing dimension.\n\nRestrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: they are declared terms the parties agree to when the\n agent accepts the offer. The AGENT self-selects the term whose restrictions it\n can honour and bears compliance. The Exchange never enforces a restriction:\n it is NOT an Exchange-side gate the requester must pass to see or buy a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and by the scopes in the requester's signed\n request — see LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (fora.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — FORA user/organization categories")).max(8).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.\n At most 8, and this list is the one of the three that does NOT carry the\n contract's usual 64: only one restriction per axis is valid, the axis enum\n is defined-only, so four is the longest conformant list and eight leaves\n room for an axis this version does not have. Like the caps on quotas and\n obligations, this one bounds the DOCUMENT — how many restrictions one term\n may carry — and not the work of checking it: a validator walks every element\n it is handed before any cardinality rule is reported, so an over-cap list is\n traversed in full on its way to being refused.\n\nWhat makes this list different is that one rule walks it against ITSELF. The\n one-per-kind rule below is quadratic, so it carries its own size test and\n stays silent above this cap; a conformance guard holds the two numbers equal,\n because a cap raised without the test would leave the lists in between\n unchecked for duplicate axes and accepted. The neighbouring disjointness rule\n on each element is quadratic only in that element's two token lists, both\n capped at 64, so its cost is bounded per restriction and linear across the\n list — it needs no such test.").optional(), "scopes": z.array(z.string()).max(64).describe("Scope-gating: the Exchange presents an offer for this term only when the\n scopes in the requester's signed request (Requester.scopes) cover ALL of\n these scopes (AND-semantics). Empty = public. A subscription term is\n Pricing{model:FREE} + scopes:[\"subscription:...\"].\n\nRequester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n instead gate a term behind a verified delegation (a subscription term is\n the usual case): then only the delegation chain's verified effective scopes\n are matched against these scopes, and Requester.scopes is ignored for that\n term. To a requester that presents no valid delegation, it answers as if\n the term did not exist, as existence hiding requires (see\n Requester.scopes). An offer the Exchange presented is honoured until it\n expires, and a gated offer expires no later than the chain's effective\n expiry (see Delegation).\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments (at least one), and there is NO implicit prefix match\n (a grant narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("LicenseTerm — Universal licensing unit.\n\nOne LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation — with one move: on an offer the\n term's price is Offer.pricing, and the term itself carries none.\n\n Validation rules:\n - Pricing MUST be present on EVERY catalog term (ResourceEntry.terms),\n regardless of semantics — the resource_entry.terms.pricing_required\n rule. Absent Pricing → reject at ingest: an agent cannot act on a term\n with no price. This holds for REFERENCE_ONLY too — its License governs\n the human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - Pricing MUST be unset on the term an offer carries (Offer.terms) — the\n offer.terms.pricing_unset rule. The offer's price is Offer.pricing, and\n each offer derives from exactly one catalog term, so the price is stated\n once and cannot disagree with itself. The requirement is therefore a\n rule of the message that holds the term, not of LicenseTerm.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - A PER_UNIT term MAY state Pricing.estimated_quantity. It is not\n required, on the pushed term or on the offer it projects to — see\n Pricing.estimated_quantity.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).min(1).max(1).describe("The licensing term this offer sells, sourced from the publisher's\n ResourceEntry. EXACTLY ONE, and the bound is enforced rather than asked\n for: an offer IS a single licensing arrangement, so N terms on a resource\n project to N offers on that resource, each selling one of them — see\n `pricing` above. An offer with no term would be an offer projected from\n nothing, which is why the floor is one and not zero. The field is\n `repeated` because it shipped that way in v1 and renumbering a released\n field is not available to us, so the cardinality rides as a validation\n rule instead. `ResourceEntry.terms` is the plural side, bounded at 32: a\n resource carries many terms, an offer sells one of them.\n\nThe distinction is not cosmetic. `pricing` is what a Broker ranks on and\n what execute charges, so a term that is not on its own offer has no price\n a Broker can compare and no offer_id an agent can buy. Fusing several\n terms onto one offer makes every term but the first unsellable — a\n dual-licensed resource sells only under whichever arrangement the\n publisher happened to store first.\n\n The term here carries everything the publisher declared EXCEPT its price:\n its `pricing` MUST be unset (the offer.terms.pricing_unset rule), because\n the offer's price is `pricing` above. Its semantics, license,\n restrictions, quotas, obligations, scopes and part_label are the\n publisher's, unchanged.\n\n Where a term is reachable only under an existing subscription, its offer\n carries `subscription_id` and prices at zero marginal cost; the terms a\n requester may see at all are selected by `LicenseTerm.scopes`.\n\n See: Universal Licensing Core section.").optional(), "title": z.string().describe("Resource title (human-readable, for display/logging).").optional() }).describe("Offer — A single resource offer from an Exchange.\n\nCombines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via fora-comp-v1 extension profile.")).describe("Zero or more offers for this URI. Empty = resource not available.").optional(), "restriction_filters": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("When absence_reason = RESTRICTION_FILTERED, the restriction axes that drove\n the convenience pre-filter, in the same RestrictionKind vocabulary the terms\n use (e.g. [GEOGRAPHY] when the requester's stated geography matched no term).\n Advisory diagnostics, not an enforcement verdict.").optional(), "uri": z.string().describe("The URI this group of offers is for (echoed from ResourceQuery.uris).").default("") }).describe("OfferGroup — Offers for a single requested URI.\n Enables multi-URI batch queries where the caller needs to know\n which offers correspond to which requested resource.")).describe("Offers grouped by requested URI (for multi-URI batch queries).\n When populated, `offers` SHOULD be empty to avoid ambiguity.").optional(), "offers": z.array(z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"fora.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("ResourceAttestation — Signed envelope of claims from a trusted party.\n\nA provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"fora.attestation.claims_schema\"].")).describe("Signed attestations about the resource at this URI.\n Attestations provide cryptographic proof of\n resource properties from trusted parties (providers or verification vendors).\n\nThree verification levels determine what is independently verifiable:\n Level 0 (no attestations): Resource may carry identifiers (DOI, IPTC GUID)\n for identification, but nothing is cryptographically verifiable.\n Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): Provider signs own claims with Ed25519 key.\n Agent can independently verify content hash and token count.\n CDN delivery failure + content hash mismatch are auto-disputable.\n Level 2 (third-party attested): Independent verification vendor crawled\n the resource and attested to its properties. Agent trusts the attestation\n (does not re-verify hash). Token count discrepancy is auto-disputable\n when corroborated by CDN response size.\n\n Multiple attestations may be present (e.g., provider self-attestation\n plus a third-party verification). Agents choose which to trust.").optional(), "data_as_of": z.string().datetime({ offset: true }).describe("When the offered data was current. For dynamic resources\n (resource_mutability = DYNAMIC), this is the snapshot timestamp.\n Enables the Broker to evaluate freshness: \"this credit report\n reflects data as of March 18\" or \"this drug database was updated today.\"\n\nNot set for STATIC resources (content doesn't change) or LIVE\n resources (content doesn't exist yet).\n\n The Broker compares this against RequestConstraints.max_data_age\n to filter stale offers. Example: agent requests max_data_age = 7 days,\n Broker drops offers where now() - data_as_of > 7 days.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource will be delivered.").default(0), "exchange": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("REQUIRED. Bare host of the Exchange that issued this offer (e.g.\n \"exchange.example\" or \"exchange.example:8081\"), in the form \"Request\n recipient\" defines in the file header. This is the execute-routing target:\n the agent, or a relaying Broker, sends the ExecuteTransaction call for this\n offer to this Exchange, and a Broker relaying a mixed batch groups the items\n by this value. Because it is an ordinary Offer field it falls inside the\n signed bytes (see `signature` below — the signature covers every field\n except `signature` / `signature_algorithm`), so an intermediary cannot\n redirect the execute call to a different Exchange without invalidating the\n offer, and it is what retires the X-FORA-Exchange-Endpoint transport header.\n It is also the audience statement of an ExecuteTransaction, which is why\n TransactionRequest carries no top-level `exchange`: on receipt, an Exchange\n MUST reject the request unless EVERY item's offer.exchange names its own\n domain. Presence is enforced because an empty value is unroutable — a\n relaying Broker has nothing to group or dial on, and the swap-protection\n above is vacuous when the signed bytes carry no recipient at all."), "expires_at": z.string().datetime({ offset: true }).describe("When this offer expires (ISO 8601). An offer for a term gated behind a\n verified delegation MUST NOT expire later than that delegation chain's\n effective expiry, the earliest `exp` in the chain (see Delegation).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "iab_categories": z.array(z.string()).describe("IAB Content Taxonomy category codes.\n Enables agents to filter offers by topic (e.g., \"only finance resources\").\n Uses IAB Content Taxonomy 3.1 codes.").optional(), "identity": z.object({ "c2pa_manifest": z.string().describe("C2PA content credentials manifest URI.\n Points to a sidecar or embedded C2PA manifest for this resource.\n C2PA-aware agents MAY follow this URI to validate the full provenance\n chain (creator identity, transformation history, ingredient composition)\n using C2PA libraries (JUMBF/COSE Sign1). C2PA-unaware agents can rely\n on c2pa_status and c2pa-bridged attestation claims instead.\n\nFormats:\n Sidecar: HTTPS URI to a .c2pa manifest file\n Embedded: same URI as canonical_url (manifest is inside the asset)\n Content Credentials Cloud: https://contentcredentials.org/verify?uri=...").optional(), "c2pa_status": z.enum(["C2PA_STATUS_TRUSTED","C2PA_STATUS_VALID","C2PA_STATUS_INVALID","C2PA_STATUS_ABSENT"]).describe("The full C2PA validation details (signer identity, trust list,\n action history, training/mining status) are carried in a\n ResourceAttestation with c2pa.* claims — see fora-c2pa-v1 profile.").optional(), "canonical_url": z.string().describe("Provider's authoritative URL for this resource (rel=\"canonical\").\n Always available. Different per provider for syndicated content.").optional(), "content_hash": z.string().describe("Hash of the content. Interpretation depends on hash_method:\n \"simhash-v1\" → locality-sensitive hash, for fuzzy dedup (Level 1)\n \"sha256\" → exact-match integrity hash (Level 2)\n\nLevel 1 (SimHash): computed by Exchange from extracted text.\n Agent verifies that fetched content is \"substantially similar.\"\n Tolerates dynamic page elements.\n\n Level 2 (SHA-256): computed by provider from deterministic payload.\n Agent verifies exact match. Requires provider to serve consistent\n content (e.g., API endpoint, static HTML, structured JSON).\n Mismatch = dispute. Commands premium pricing.").optional(), "doi": z.string().describe("Digital Object Identifier — persistent, never changes.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().describe("Hash algorithm and verification level.\n Examples: \"simhash-v1\", \"minhash-v1\", \"sha256\", \"sha384\"").optional(), "iptc_guid": z.string().describe("IPTC NewsML-G2 globally unique identifier.\n Present when resource flows through news wire syndication (AP, Reuters).").optional(), "isni": z.string().describe("International Standard Name Identifier for the creator.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Drives hash verification behavior:\n STATIC: content_hash is stable. Agent SHOULD verify delivered content matches.\n DYNAMIC: content changes between offer and fetch (credit reports, drug databases).\n content_hash reflects state at offer generation time. Hash mismatch is\n expected and MUST NOT trigger automatic dispute.\n LIVE: content does not exist at offer time (streaming feeds, live broadcasts).\n content_hash is not applicable. The \"resource\" is the stream endpoint.\n\n Validated across 18 use cases: static content (articles, patents, legislation),\n dynamic data (credit reports, drug interactions, stock snapshots), and live\n streams (MarketData quotes, NPR broadcast, news monitoring feeds)."), "soft_binding": z.string().describe("Soft binding hash — content-derived identifier that survives format\n transcoding (resolution changes, compression, PDF-to-text extraction).\n Extracted from C2PA soft binding assertion when present.\n Enables post-delivery verification when the hard binding hash breaks\n due to legitimate format conversion.\n\nAlgorithm specified in soft_binding_method. Values are algorithm-specific\n (e.g., perceptual hash hex string, watermark identifier).").optional(), "soft_binding_method": z.string().describe("Algorithm used for soft_binding.\n Examples: \"phash-v1\" (perceptual hash), \"c2pa-watermark\" (C2PA invisible\n watermark), \"chromaprint\" (audio fingerprint).").optional() }).describe("Resource identity for cross-exchange deduplication.\n Enables Brokers to recognize the same resource offered by\n different Exchanges and compare pricing.").optional(), "offer_id": z.string().describe("Unique identifier for this offer, assigned by the Exchange.\n Opaque to the caller: not derived from the resource, its URL, or any\n other field, and carries no meaning beyond identifying this offer.\n Two offers for the same resource have different offer_ids.").default(""), "previews": z.array(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("Preview — Lightweight resource preview for offer evaluation.\n\nThe Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no FORA transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")).describe("Lightweight previews for offer evaluation.\n The Exchange holds URLs (50–200 bytes each); the provider's CDN serves\n the actual bytes. Agents fetch previews only when evaluating offers —\n not on every discovery query. Multiple previews at different sizes\n allow agents to pick the cheapest fetch for their evaluation needs.\n\nPer content type:\n Image: watermarked thumbnail (150–450px JPEG)\n Video: short clip (10–30s MP4, watermarked)\n Audio: short clip (15–30s MP3, low-bitrate or watermarked)\n Text: snippet or abstract (first 200 words as text/plain)\n Data: sample records (1–3 rows as application/json)\n Stream: optional frame capture or none (streams are priced by time)\n\n Modeled after Shutterstock (multi-size thumbnail URLs),\n Spotify (preview_url to 30s clip), IIIF (parameterized image URLs),\n and OpenRTB native (img.url + dimensions).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("The offer's price, and its ONLY price. An offer represents a single\n licensing arrangement: each projected LicenseTerm yields its own offer, and\n the Exchange moves that term's price here, so the term inside `terms`\n carries no pricing of its own (the offer.terms.pricing_unset rule). This is\n the price execute charges, the price a Broker ranks and compares across\n Exchanges, and the price a metered purchase is charged at. A resource with\n multiple alternative terms (e.g. dual-licensed) produces multiple separate\n offers, one per term — never one offer with a \"headline\" picked among them.\n\nIt derives from exactly one catalog term: the term's pricing as the\n publisher declared it (ResourceEntry.terms). Each offer derives from one\n term, and the offer carries no second copy, so the offer's price and its\n term's price cannot disagree.\n\n On a metered (PER_UNIT) offer, the rate and the estimate a metered\n purchase is charged at are read from here (see Pricing). The estimate is\n optional. When the publisher states one — on the term's own pricing, or\n once for the resource on ResourceEntry.estimated_quantity — the Exchange\n carries it here as estimated_quantity, the term's own estimate taking\n precedence. When the publisher states none, the offer may carry none, and\n the purchase then charges one unit.").optional(), "reporting": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds). A report that arrives after\n the window closed is refused with USAGE_REPORT_REJECTION_REASON_WINDOW_EXPIRED\n (Connect code failed_precondition).").optional() }).describe("Post-usage reporting requirements for this offer.").optional(), "signature": z.string().describe("REQUIRED. Hex-encoded detached Ed25519 signature over the canonical\n serialization of the ENTIRE Offer — every field, including `pricing`,\n `terms` (the full licensing payload), `expires_at`, and `exchange`. Only\n `signature` and `signature_algorithm` are excluded from the signed bytes.\n `expires_at` is signed so the offer's validity window is\n integrity-protected: a relaying Broker cannot extend (or shorten) the TTL\n of a signed offer to replay it outside the window the Exchange intended.\n\nCANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n UNKNOWN FIELDS. A canonicalizer either OMITS content it has no schema for or\n PRESERVES it, and the rule follows from which:\n\n - OMITTING (e.g. proto-JSON, which emits only schema-defined fields): such a\n canonicalizer CANNOT reproduce the signed bytes of a message carrying\n unknown fields — what it renders silently drops part of what the signer\n covered. It MUST refuse the message rather than emit the reduced bytes,\n and a verifier built on it MUST reject rather than verify over them. The\n refusal binds at EVERY depth: a nested message and each element of a\n repeated or map field carries its own unknown-field set.\n - PRESERVING (a canonicalizer that carries unrecognized members through):\n it reproduces the signed bytes faithfully, so there is nothing to refuse.\n\n Either way an APPENDED field cannot pass: an omitting canonicalizer refuses\n the message, and a preserving one renders the appended member into bytes the\n signer never covered, so the signature fails. Without the refusal the omitting\n case would fail OPEN — an intermediary could add unknown fields to an\n already-signed message and leave its signature verifying, smuggling\n unauthenticated content through a message the recipient treats as verified.\n\n Extensions therefore ride in `ext` / `ext_critical`, which are defined fields\n and inside the signed bytes — never as undeclared field numbers.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.").default(""), "signature_algorithm": z.string().describe("JOSE/JWA algorithm identifier (RFC 8037 §3.1). Always 'EdDSA' for\n Ed25519. Advisory only: this field is cleared before the canonical\n payload is signed, so it is not covered by the signature.").default(""), "subscription_id": z.string().describe("If set, this offer is available under an existing subscription/deal.\n No per-request billing — usage tracked against subscription quota.\n Pricing.rate = \"0\" for subscription offers (zero marginal cost).\n The Broker SHOULD prefer subscription offers when available.").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("SubscriptionQuotaInfo — Proactive quota signaling for subscription access.\n\nAnalogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Subscription quota state, when this offer is under a subscription.\n Enables the agent to see remaining quota before committing.\n Multiple entries when the subscription has independent quotas\n (e.g., access count + spend cap).").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Obligation — A post-use behavioral requirement attached to a LicenseTerm.\n\nExamples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).max(64).describe("Post-use behavioral requirements.\n At most 64, for the reason quotas carries.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. Where it is required depends on the message that\n holds the term, so the requirement is a rule of that message rather than of\n this field:\n - On a catalog term (ResourceEntry.terms) it is REQUIRED regardless of\n semantics — an agent cannot act on a priceless term, so absent Pricing\n is a validation error at ingest (resource_entry.terms.pricing_required).\n model = FREE must be stated explicitly (absent Pricing is not free). A\n REFERENCE_ONLY term states its price here too; its License governs the\n human-readable terms but does not replace the machine-readable price.\n - On the term an offer carries (Offer.terms) it MUST be unset\n (offer.terms.pricing_unset): the offer's price is Offer.pricing, stated\n once.").optional(), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The unit being capped — an open vocabulary axis.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quota — A usage cap the term declares.\n\nA quota states how much a licensee may consume under the term before the\n term expires or must be renegotiated. Like a restriction, it is a declared\n term the parties agree to. Quotas are NOT billing quantities — billing is in\n Pricing.\n\n The metric vocabulary is authored ONLY in the (fora.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).max(64).describe("Usage caps. The agent must not exceed any individual Quota.\n At most 64, the bound every per-message list in this contract carries when\n no rule walks it more than once. It bounds what one term may carry, not the\n work of checking one — a validator walks every element it is handed before\n the cap is reported, so the cost of checking is bounded at the transport.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to. Defined-only: the axis set is\n CLOSED, and a number outside it is refused rather than ignored. A custom\n axis is RESTRICTION_KIND_OTHER, whose meaning rides in permitted/prohibited,\n so a new number was never the extension mechanism — accepting one would\n admit a restriction no consumer can evaluate onto a term whose default is\n BINDING (see advisory below), which fails open on the axis a publisher most\n needs honoured. Closing the axis does NOT bound the cost of the one-per-kind\n rule below, and must not be read as doing so: a number this rule refuses is\n still distinct from every other, so that rule's all() finds no duplicate to\n stop on and walks the list in full anyway. Its cost is bounded by the size\n test the rule itself carries."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restriction — A single constraint on one licensing dimension.\n\nRestrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: they are declared terms the parties agree to when the\n agent accepts the offer. The AGENT self-selects the term whose restrictions it\n can honour and bears compliance. The Exchange never enforces a restriction:\n it is NOT an Exchange-side gate the requester must pass to see or buy a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and by the scopes in the requester's signed\n request — see LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (fora.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — FORA user/organization categories")).max(8).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.\n At most 8, and this list is the one of the three that does NOT carry the\n contract's usual 64: only one restriction per axis is valid, the axis enum\n is defined-only, so four is the longest conformant list and eight leaves\n room for an axis this version does not have. Like the caps on quotas and\n obligations, this one bounds the DOCUMENT — how many restrictions one term\n may carry — and not the work of checking it: a validator walks every element\n it is handed before any cardinality rule is reported, so an over-cap list is\n traversed in full on its way to being refused.\n\nWhat makes this list different is that one rule walks it against ITSELF. The\n one-per-kind rule below is quadratic, so it carries its own size test and\n stays silent above this cap; a conformance guard holds the two numbers equal,\n because a cap raised without the test would leave the lists in between\n unchecked for duplicate axes and accepted. The neighbouring disjointness rule\n on each element is quadratic only in that element's two token lists, both\n capped at 64, so its cost is bounded per restriction and linear across the\n list — it needs no such test.").optional(), "scopes": z.array(z.string()).max(64).describe("Scope-gating: the Exchange presents an offer for this term only when the\n scopes in the requester's signed request (Requester.scopes) cover ALL of\n these scopes (AND-semantics). Empty = public. A subscription term is\n Pricing{model:FREE} + scopes:[\"subscription:...\"].\n\nRequester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n instead gate a term behind a verified delegation (a subscription term is\n the usual case): then only the delegation chain's verified effective scopes\n are matched against these scopes, and Requester.scopes is ignored for that\n term. To a requester that presents no valid delegation, it answers as if\n the term did not exist, as existence hiding requires (see\n Requester.scopes). An offer the Exchange presented is honoured until it\n expires, and a gated offer expires no later than the chain's effective\n expiry (see Delegation).\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments (at least one), and there is NO implicit prefix match\n (a grant narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("LicenseTerm — Universal licensing unit.\n\nOne LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation — with one move: on an offer the\n term's price is Offer.pricing, and the term itself carries none.\n\n Validation rules:\n - Pricing MUST be present on EVERY catalog term (ResourceEntry.terms),\n regardless of semantics — the resource_entry.terms.pricing_required\n rule. Absent Pricing → reject at ingest: an agent cannot act on a term\n with no price. This holds for REFERENCE_ONLY too — its License governs\n the human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - Pricing MUST be unset on the term an offer carries (Offer.terms) — the\n offer.terms.pricing_unset rule. The offer's price is Offer.pricing, and\n each offer derives from exactly one catalog term, so the price is stated\n once and cannot disagree with itself. The requirement is therefore a\n rule of the message that holds the term, not of LicenseTerm.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - A PER_UNIT term MAY state Pricing.estimated_quantity. It is not\n required, on the pushed term or on the offer it projects to — see\n Pricing.estimated_quantity.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).min(1).max(1).describe("The licensing term this offer sells, sourced from the publisher's\n ResourceEntry. EXACTLY ONE, and the bound is enforced rather than asked\n for: an offer IS a single licensing arrangement, so N terms on a resource\n project to N offers on that resource, each selling one of them — see\n `pricing` above. An offer with no term would be an offer projected from\n nothing, which is why the floor is one and not zero. The field is\n `repeated` because it shipped that way in v1 and renumbering a released\n field is not available to us, so the cardinality rides as a validation\n rule instead. `ResourceEntry.terms` is the plural side, bounded at 32: a\n resource carries many terms, an offer sells one of them.\n\nThe distinction is not cosmetic. `pricing` is what a Broker ranks on and\n what execute charges, so a term that is not on its own offer has no price\n a Broker can compare and no offer_id an agent can buy. Fusing several\n terms onto one offer makes every term but the first unsellable — a\n dual-licensed resource sells only under whichever arrangement the\n publisher happened to store first.\n\n The term here carries everything the publisher declared EXCEPT its price:\n its `pricing` MUST be unset (the offer.terms.pricing_unset rule), because\n the offer's price is `pricing` above. Its semantics, license,\n restrictions, quotas, obligations, scopes and part_label are the\n publisher's, unchanged.\n\n Where a term is reachable only under an existing subscription, its offer\n carries `subscription_id` and prices at zero marginal cost; the terms a\n requester may see at all are selected by `LicenseTerm.scopes`.\n\n See: Universal Licensing Core section.").optional(), "title": z.string().describe("Resource title (human-readable, for display/logging).").optional() }).describe("Offer — A single resource offer from an Exchange.\n\nCombines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via fora-comp-v1 extension profile.")).describe("Flat list of offers (for single-URI queries).").optional(), "rate_limit": z.object({ "limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Maximum requests allowed in the current window.").optional(), "remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Requests remaining in the current window.").optional(), "reset_at": z.string().datetime({ offset: true }).describe("When the current window resets (UTC). After this time, `remaining` resets to `limit`.").optional(), "window": z.string().describe("Duration of the rate limit window (e.g. 60s = per-minute limit).").optional() }).describe("Rate limit status for this caller.\n Present when the Exchange enforces per-caller rate limits on discovery.\n Enables agents/Brokers to throttle proactively rather than hitting\n hard limits. Particularly important when a Broker fans out the\n same batch query to multiple Exchanges — mid-batch rate limiting\n can cause partial results if not signaled early.").optional(), "ver": z.string().describe("FORA protocol version — \"1.0\". Stamped by the sender from a single\n constant; advisory on receive. See \"Protocol version\" in the file header.").default("") }).describe("ResourceResponse — Exchange returns candidate resource offers.\n\nWhen the ResourceQuery contains multiple URIs, offers are grouped by URI\n via OfferGroup. When a single URI is queried, the Exchange MAY use\n either the flat `offers` field or a single OfferGroup.")))(); export const RestrictionSchema = /* @__PURE__ */ (() => wire(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to. Defined-only: the axis set is\n CLOSED, and a number outside it is refused rather than ignored. A custom\n axis is RESTRICTION_KIND_OTHER, whose meaning rides in permitted/prohibited,\n so a new number was never the extension mechanism — accepting one would\n admit a restriction no consumer can evaluate onto a term whose default is\n BINDING (see advisory below), which fails open on the axis a publisher most\n needs honoured. Closing the axis does NOT bound the cost of the one-per-kind\n rule below, and must not be read as doing so: a number this rule refuses is\n still distinct from every other, so that rule's all() finds no duplicate to\n stop on and walks the list in full anyway. Its cost is bounded by the size\n test the rule itself carries."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restriction — A single constraint on one licensing dimension.\n\nRestrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: they are declared terms the parties agree to when the\n agent accepts the offer. The AGENT self-selects the term whose restrictions it\n can honour and bears compliance. The Exchange never enforces a restriction:\n it is NOT an Exchange-side gate the requester must pass to see or buy a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and by the scopes in the requester's signed\n request — see LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (fora.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — FORA user/organization categories")))(); @@ -200,9 +200,9 @@ export const TermSemanticsSchema = /* @__PURE__ */ (() => wire(z.enum(["TERM_SEM export const TransactionDenialSchema = /* @__PURE__ */ (() => wire(z.object({ "exchange": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("Bare host of the Exchange that PRODUCED this denial, in the form \"Request\n recipient\" defines in the file header. Not an echo of what the caller sent:\n on a relayed or fanned-out execute the request went to a Broker, so the\n Exchange that refused may not be one the agent named (through\n BrokerService.ExecuteTransaction this detail reaches the agent inside\n UpstreamRefusal.detail, beside UpstreamRefusal.party). Carrying it here is\n what lets ACCOUNT_NOT_REGISTERED be actionable — the agent learns where to\n call Register without fetching a manifest to work it out. NOTHING SIGNS THIS\n VALUE: it rides in a response, and on a relayed path the response passed\n through an intermediary, so this field is exactly the unsigned addressing\n the request-side `exchange` field exists to refuse. Treat it as a HINT, not\n an instruction. Before acting on it — and registering is a consequential act,\n handing an operator's business data and a signed acceptance of that\n Exchange's terms to whoever answers — a caller MUST check the value against\n a domain it already trusts for this transaction: the signed `offer.exchange`\n of the items in the refused request, or its own RequestConstraints.exchanges\n set. A value matching neither is reported to the caller and never dialled,\n because a hostile intermediary that could choose it would be choosing where\n an unattended agent registers.").optional(), "offer_id": z.string().describe("DEPRECATED, never set. A whole-request refusal pertains to no one offer; a\n denial of one offer rides on that offer's TransactionResultItem. The field\n is retained because removing it would break the v1 wire contract; a\n receiver ignores it.").optional(), "reason": z.enum(["DENIAL_REASON_ACCOUNT_INACTIVE","DENIAL_REASON_INSUFFICIENT_BALANCE","DENIAL_REASON_RATE_LIMITED","DENIAL_REASON_CONTENT_UNAVAILABLE","DENIAL_REASON_RESTRICTION_NOT_SATISFIED","DENIAL_REASON_REPORTING_OVERDUE","DENIAL_REASON_OFFER_EXPIRED","DENIAL_REASON_SIGNATURE_INVALID","DENIAL_REASON_QUOTA_EXCEEDED","DENIAL_REASON_DELEGATION_INVALID","DENIAL_REASON_SCOPE_INSUFFICIENT","DENIAL_REASON_ENTITLEMENT_MISSING","DENIAL_REASON_ENTITLEMENT_MALFORMED","DENIAL_REASON_ENTITLEMENT_EXPIRED","DENIAL_REASON_ENTITLEMENT_WRONG_BUYER","DENIAL_REASON_SUBSCRIPTION_LAPSED","DENIAL_REASON_ENTITLEMENT_NOT_GRANTED","DENIAL_REASON_ACCOUNT_NOT_REGISTERED","DENIAL_REASON_RELAY_NOT_ACCEPTED","DENIAL_REASON_UNKNOWN_CRITICAL_EXTENSION"]).describe("The denial reason (defined-only, non-zero)"), "restriction_mismatches": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("DEPRECATED, never set. RESTRICTION_NOT_SATISFIED is never sent: the\n Exchange never enforces a restriction (see Restriction). The field is\n retained because removing it would break the v1 wire contract; a receiver\n ignores it.").optional() }).describe("TransactionDenial — ExecuteTransaction refused the WHOLE request. Reuses the\n DenialReason vocabulary.\n\nThis is the only case an ExecuteTransaction answer is a non-OK error carrying\n a DenialReason: the Exchange refuses the request as a whole and decides no\n item, for a reason about the requester or the request that would deny every\n item alike — for example the requester holds no account\n (ACCOUNT_NOT_REGISTERED), its account is not active (ACCOUNT_INACTIVE), it has\n overdue reports (REPORTING_OVERDUE), or the call is rate limited\n (RATE_LIMITED). On a purchase a Broker relayed, the requester is the agent and\n the account is the agent's, never the Broker's (see \"Accounts\" in the file\n header). Nothing is\n purchased. Every decision about an item is answered in the body instead, on\n TransactionResultItem.denial_reason, whatever the item count: a denied\n one-item purchase is a successful response whose one item is denied, never\n this error. RELAY_NOT_ACCEPTED is a per-item decision and is never carried\n here, and RESTRICTION_NOT_SATISFIED is never sent at all.")))(); -export const TransactionItemSchema = /* @__PURE__ */ (() => wire(z.object({ "agent_acceptance": z.object({ "signature": z.string().min(1).describe("Hex-encoded detached Ed25519 signature over the canonical AgentAcceptancePayload\n bytes (see the canonical-signing definition on Offer.signature)."), "signature_algorithm": z.string().describe("Signature algorithm; \"EdDSA\" for Ed25519.").default("") }).describe("The agent's detached acceptance signature over this item's `offer`.\n Optional on the wire; the Exchange enforces presence per\n item at the service layer for relayed batches. Signed bytes = the canonical\n AgentAcceptancePayload form, with requester_* and idempotency_key\n taken from the ENCLOSING TransactionRequest and offer_sig = offer.signature.").optional(), "offer": z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"fora.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("ResourceAttestation — Signed envelope of claims from a trusted party.\n\nA provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"fora.attestation.claims_schema\"].")).describe("Signed attestations about the resource at this URI.\n Attestations provide cryptographic proof of\n resource properties from trusted parties (providers or verification vendors).\n\nThree verification levels determine what is independently verifiable:\n Level 0 (no attestations): Resource may carry identifiers (DOI, IPTC GUID)\n for identification, but nothing is cryptographically verifiable.\n Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): Provider signs own claims with Ed25519 key.\n Agent can independently verify content hash and token count.\n CDN delivery failure + content hash mismatch are auto-disputable.\n Level 2 (third-party attested): Independent verification vendor crawled\n the resource and attested to its properties. Agent trusts the attestation\n (does not re-verify hash). Token count discrepancy is auto-disputable\n when corroborated by CDN response size.\n\n Multiple attestations may be present (e.g., provider self-attestation\n plus a third-party verification). Agents choose which to trust.").optional(), "data_as_of": z.string().datetime({ offset: true }).describe("When the offered data was current. For dynamic resources\n (resource_mutability = DYNAMIC), this is the snapshot timestamp.\n Enables the Broker to evaluate freshness: \"this credit report\n reflects data as of March 18\" or \"this drug database was updated today.\"\n\nNot set for STATIC resources (content doesn't change) or LIVE\n resources (content doesn't exist yet).\n\n The Broker compares this against RequestConstraints.max_data_age\n to filter stale offers. Example: agent requests max_data_age = 7 days,\n Broker drops offers where now() - data_as_of > 7 days.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource will be delivered.").default(0), "exchange": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("REQUIRED. Bare host of the Exchange that issued this offer (e.g.\n \"exchange.example\" or \"exchange.example:8081\"), in the form \"Request\n recipient\" defines in the file header. This is the execute-routing target:\n the agent, or a relaying Broker, sends the ExecuteTransaction call for this\n offer to this Exchange, and a Broker relaying a mixed batch groups the items\n by this value. Because it is an ordinary Offer field it falls inside the\n signed bytes (see `signature` below — the signature covers every field\n except `signature` / `signature_algorithm`), so an intermediary cannot\n redirect the execute call to a different Exchange without invalidating the\n offer, and it is what retires the X-FORA-Exchange-Endpoint transport header.\n It is also the audience statement of an ExecuteTransaction, which is why\n TransactionRequest carries no top-level `exchange`: on receipt, an Exchange\n MUST reject the request unless EVERY item's offer.exchange names its own\n domain. Presence is enforced because an empty value is unroutable — a\n relaying Broker has nothing to group or dial on, and the swap-protection\n above is vacuous when the signed bytes carry no recipient at all."), "expires_at": z.string().datetime({ offset: true }).describe("When this offer expires (ISO 8601).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "iab_categories": z.array(z.string()).describe("IAB Content Taxonomy category codes.\n Enables agents to filter offers by topic (e.g., \"only finance resources\").\n Uses IAB Content Taxonomy 3.1 codes.").optional(), "identity": z.object({ "c2pa_manifest": z.string().describe("C2PA content credentials manifest URI.\n Points to a sidecar or embedded C2PA manifest for this resource.\n C2PA-aware agents MAY follow this URI to validate the full provenance\n chain (creator identity, transformation history, ingredient composition)\n using C2PA libraries (JUMBF/COSE Sign1). C2PA-unaware agents can rely\n on c2pa_status and c2pa-bridged attestation claims instead.\n\nFormats:\n Sidecar: HTTPS URI to a .c2pa manifest file\n Embedded: same URI as canonical_url (manifest is inside the asset)\n Content Credentials Cloud: https://contentcredentials.org/verify?uri=...").optional(), "c2pa_status": z.enum(["C2PA_STATUS_TRUSTED","C2PA_STATUS_VALID","C2PA_STATUS_INVALID","C2PA_STATUS_ABSENT"]).describe("The full C2PA validation details (signer identity, trust list,\n action history, training/mining status) are carried in a\n ResourceAttestation with c2pa.* claims — see fora-c2pa-v1 profile.").optional(), "canonical_url": z.string().describe("Provider's authoritative URL for this resource (rel=\"canonical\").\n Always available. Different per provider for syndicated content.").optional(), "content_hash": z.string().describe("Hash of the content. Interpretation depends on hash_method:\n \"simhash-v1\" → locality-sensitive hash, for fuzzy dedup (Level 1)\n \"sha256\" → exact-match integrity hash (Level 2)\n\nLevel 1 (SimHash): computed by Exchange from extracted text.\n Agent verifies that fetched content is \"substantially similar.\"\n Tolerates dynamic page elements.\n\n Level 2 (SHA-256): computed by provider from deterministic payload.\n Agent verifies exact match. Requires provider to serve consistent\n content (e.g., API endpoint, static HTML, structured JSON).\n Mismatch = dispute. Commands premium pricing.").optional(), "doi": z.string().describe("Digital Object Identifier — persistent, never changes.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().describe("Hash algorithm and verification level.\n Examples: \"simhash-v1\", \"minhash-v1\", \"sha256\", \"sha384\"").optional(), "iptc_guid": z.string().describe("IPTC NewsML-G2 globally unique identifier.\n Present when resource flows through news wire syndication (AP, Reuters).").optional(), "isni": z.string().describe("International Standard Name Identifier for the creator.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Drives hash verification behavior:\n STATIC: content_hash is stable. Agent SHOULD verify delivered content matches.\n DYNAMIC: content changes between offer and fetch (credit reports, drug databases).\n content_hash reflects state at offer generation time. Hash mismatch is\n expected and MUST NOT trigger automatic dispute.\n LIVE: content does not exist at offer time (streaming feeds, live broadcasts).\n content_hash is not applicable. The \"resource\" is the stream endpoint.\n\n Validated across 18 use cases: static content (articles, patents, legislation),\n dynamic data (credit reports, drug interactions, stock snapshots), and live\n streams (MarketData quotes, NPR broadcast, news monitoring feeds)."), "soft_binding": z.string().describe("Soft binding hash — content-derived identifier that survives format\n transcoding (resolution changes, compression, PDF-to-text extraction).\n Extracted from C2PA soft binding assertion when present.\n Enables post-delivery verification when the hard binding hash breaks\n due to legitimate format conversion.\n\nAlgorithm specified in soft_binding_method. Values are algorithm-specific\n (e.g., perceptual hash hex string, watermark identifier).").optional(), "soft_binding_method": z.string().describe("Algorithm used for soft_binding.\n Examples: \"phash-v1\" (perceptual hash), \"c2pa-watermark\" (C2PA invisible\n watermark), \"chromaprint\" (audio fingerprint).").optional() }).describe("Resource identity for cross-exchange deduplication.\n Enables Brokers to recognize the same resource offered by\n different Exchanges and compare pricing.").optional(), "offer_id": z.string().describe("Unique identifier for this offer, assigned by the Exchange.\n Opaque to the caller: not derived from the resource, its URL, or any\n other field, and carries no meaning beyond identifying this offer.\n Two offers for the same resource have different offer_ids.").default(""), "previews": z.array(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("Preview — Lightweight resource preview for offer evaluation.\n\nThe Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no FORA transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")).describe("Lightweight previews for offer evaluation.\n The Exchange holds URLs (50–200 bytes each); the provider's CDN serves\n the actual bytes. Agents fetch previews only when evaluating offers —\n not on every discovery query. Multiple previews at different sizes\n allow agents to pick the cheapest fetch for their evaluation needs.\n\nPer content type:\n Image: watermarked thumbnail (150–450px JPEG)\n Video: short clip (10–30s MP4, watermarked)\n Audio: short clip (15–30s MP3, low-bitrate or watermarked)\n Text: snippet or abstract (first 200 words as text/plain)\n Data: sample records (1–3 rows as application/json)\n Stream: optional frame capture or none (streams are priced by time)\n\n Modeled after Shutterstock (multi-size thumbnail URLs),\n Spotify (preview_url to 30s clip), IIIF (parameterized image URLs),\n and OpenRTB native (img.url + dimensions).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("The offer's price, and its ONLY price. An offer represents a single\n licensing arrangement: each projected LicenseTerm yields its own offer, and\n the Exchange moves that term's price here, so the term inside `terms`\n carries no pricing of its own (the offer.terms.pricing_unset rule). This is\n the price execute charges, the price a Broker ranks and compares across\n Exchanges, and the price a metered purchase is charged at. A resource with\n multiple alternative terms (e.g. dual-licensed) produces multiple separate\n offers, one per term — never one offer with a \"headline\" picked among them.\n\nIt derives from exactly one catalog term: the term's pricing as the\n publisher declared it (ResourceEntry.terms). Each offer derives from one\n term, and the offer carries no second copy, so the offer's price and its\n term's price cannot disagree.\n\n On a metered (PER_UNIT) offer, the rate and the estimate a metered\n purchase is charged at are read from here (see Pricing). The estimate is\n optional. When the publisher states one — on the term's own pricing, or\n once for the resource on ResourceEntry.estimated_quantity — the Exchange\n carries it here as estimated_quantity, the term's own estimate taking\n precedence. When the publisher states none, the offer may carry none, and\n the purchase then charges one unit.").optional(), "reporting": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds). A report that arrives after\n the window closed is refused with USAGE_REPORT_REJECTION_REASON_WINDOW_EXPIRED\n (Connect code failed_precondition).").optional() }).describe("Post-usage reporting requirements for this offer.").optional(), "signature": z.string().describe("REQUIRED. Hex-encoded detached Ed25519 signature over the canonical\n serialization of the ENTIRE Offer — every field, including `pricing`,\n `terms` (the full licensing payload), `expires_at`, and `exchange`. Only\n `signature` and `signature_algorithm` are excluded from the signed bytes.\n `expires_at` is signed so the offer's validity window is\n integrity-protected: a relaying Broker cannot extend (or shorten) the TTL\n of a signed offer to replay it outside the window the Exchange intended.\n\nCANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n UNKNOWN FIELDS. A canonicalizer either OMITS content it has no schema for or\n PRESERVES it, and the rule follows from which:\n\n - OMITTING (e.g. proto-JSON, which emits only schema-defined fields): such a\n canonicalizer CANNOT reproduce the signed bytes of a message carrying\n unknown fields — what it renders silently drops part of what the signer\n covered. It MUST refuse the message rather than emit the reduced bytes,\n and a verifier built on it MUST reject rather than verify over them. The\n refusal binds at EVERY depth: a nested message and each element of a\n repeated or map field carries its own unknown-field set.\n - PRESERVING (a canonicalizer that carries unrecognized members through):\n it reproduces the signed bytes faithfully, so there is nothing to refuse.\n\n Either way an APPENDED field cannot pass: an omitting canonicalizer refuses\n the message, and a preserving one renders the appended member into bytes the\n signer never covered, so the signature fails. Without the refusal the omitting\n case would fail OPEN — an intermediary could add unknown fields to an\n already-signed message and leave its signature verifying, smuggling\n unauthenticated content through a message the recipient treats as verified.\n\n Extensions therefore ride in `ext` / `ext_critical`, which are defined fields\n and inside the signed bytes — never as undeclared field numbers.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.").default(""), "signature_algorithm": z.string().describe("JOSE/JWA algorithm identifier (RFC 8037 §3.1). Always 'EdDSA' for\n Ed25519. Advisory only: this field is cleared before the canonical\n payload is signed, so it is not covered by the signature.").default(""), "subscription_id": z.string().describe("If set, this offer is available under an existing subscription/deal.\n No per-request billing — usage tracked against subscription quota.\n Pricing.rate = \"0\" for subscription offers (zero marginal cost).\n The Broker SHOULD prefer subscription offers when available.").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("SubscriptionQuotaInfo — Proactive quota signaling for subscription access.\n\nAnalogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Subscription quota state, when this offer is under a subscription.\n Enables the agent to see remaining quota before committing.\n Multiple entries when the subscription has independent quotas\n (e.g., access count + spend cap).").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Obligation — A post-use behavioral requirement attached to a LicenseTerm.\n\nExamples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).max(64).describe("Post-use behavioral requirements.\n At most 64, for the reason quotas carries.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. Where it is required depends on the message that\n holds the term, so the requirement is a rule of that message rather than of\n this field:\n - On a catalog term (ResourceEntry.terms) it is REQUIRED regardless of\n semantics — an agent cannot act on a priceless term, so absent Pricing\n is a validation error at ingest (resource_entry.terms.pricing_required).\n model = FREE must be stated explicitly (absent Pricing is not free). A\n REFERENCE_ONLY term states its price here too; its License governs the\n human-readable terms but does not replace the machine-readable price.\n - On the term an offer carries (Offer.terms) it MUST be unset\n (offer.terms.pricing_unset): the offer's price is Offer.pricing, stated\n once.").optional(), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The unit being capped — an open vocabulary axis.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quota — A usage cap the term declares.\n\nA quota states how much a licensee may consume under the term before the\n term expires or must be renegotiated. Like a restriction, it is a declared\n term the parties agree to. Quotas are NOT billing quantities — billing is in\n Pricing.\n\n The metric vocabulary is authored ONLY in the (fora.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).max(64).describe("Usage caps. The agent must not exceed any individual Quota.\n At most 64, the bound every per-message list in this contract carries when\n no rule walks it more than once. It bounds what one term may carry, not the\n work of checking one — a validator walks every element it is handed before\n the cap is reported, so the cost of checking is bounded at the transport.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to. Defined-only: the axis set is\n CLOSED, and a number outside it is refused rather than ignored. A custom\n axis is RESTRICTION_KIND_OTHER, whose meaning rides in permitted/prohibited,\n so a new number was never the extension mechanism — accepting one would\n admit a restriction no consumer can evaluate onto a term whose default is\n BINDING (see advisory below), which fails open on the axis a publisher most\n needs honoured. Closing the axis does NOT bound the cost of the one-per-kind\n rule below, and must not be read as doing so: a number this rule refuses is\n still distinct from every other, so that rule's all() finds no duplicate to\n stop on and walks the list in full anyway. Its cost is bounded by the size\n test the rule itself carries."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restriction — A single constraint on one licensing dimension.\n\nRestrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: they are declared terms the parties agree to when the\n agent accepts the offer. The AGENT self-selects the term whose restrictions it\n can honour and bears compliance. The Exchange never enforces a restriction:\n it is NOT an Exchange-side gate the requester must pass to see or buy a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and by the scopes in the requester's signed\n request — see LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (fora.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — FORA user/organization categories")).max(8).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.\n At most 8, and this list is the one of the three that does NOT carry the\n contract's usual 64: only one restriction per axis is valid, the axis enum\n is defined-only, so four is the longest conformant list and eight leaves\n room for an axis this version does not have. Like the caps on quotas and\n obligations, this one bounds the DOCUMENT — how many restrictions one term\n may carry — and not the work of checking it: a validator walks every element\n it is handed before any cardinality rule is reported, so an over-cap list is\n traversed in full on its way to being refused.\n\nWhat makes this list different is that one rule walks it against ITSELF. The\n one-per-kind rule below is quadratic, so it carries its own size test and\n stays silent above this cap; a conformance guard holds the two numbers equal,\n because a cap raised without the test would leave the lists in between\n unchecked for duplicate axes and accepted. The neighbouring disjointness rule\n on each element is quadratic only in that element's two token lists, both\n capped at 64, so its cost is bounded per restriction and linear across the\n list — it needs no such test.").optional(), "scopes": z.array(z.string()).max(64).describe("Scope-gating: the Exchange presents an offer for this term only when the\n scopes in the requester's signed request (Requester.scopes) cover ALL of\n these scopes (AND-semantics). Empty = public. A subscription term is\n Pricing{model:FREE} + scopes:[\"subscription:...\"].\n\nRequester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n also require a verified delegation that grants these scopes before it\n presents the term. To a requester that presents none, it answers as if the\n term did not exist, as existence hiding requires (see Requester.scopes). An\n offer the Exchange presented is honoured until it expires.\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("LicenseTerm — Universal licensing unit.\n\nOne LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation — with one move: on an offer the\n term's price is Offer.pricing, and the term itself carries none.\n\n Validation rules:\n - Pricing MUST be present on EVERY catalog term (ResourceEntry.terms),\n regardless of semantics — the resource_entry.terms.pricing_required\n rule. Absent Pricing → reject at ingest: an agent cannot act on a term\n with no price. This holds for REFERENCE_ONLY too — its License governs\n the human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - Pricing MUST be unset on the term an offer carries (Offer.terms) — the\n offer.terms.pricing_unset rule. The offer's price is Offer.pricing, and\n each offer derives from exactly one catalog term, so the price is stated\n once and cannot disagree with itself. The requirement is therefore a\n rule of the message that holds the term, not of LicenseTerm.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - A PER_UNIT term MAY state Pricing.estimated_quantity. It is not\n required, on the pushed term or on the offer it projects to — see\n Pricing.estimated_quantity.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).min(1).max(1).describe("The licensing term this offer sells, sourced from the publisher's\n ResourceEntry. EXACTLY ONE, and the bound is enforced rather than asked\n for: an offer IS a single licensing arrangement, so N terms on a resource\n project to N offers on that resource, each selling one of them — see\n `pricing` above. An offer with no term would be an offer projected from\n nothing, which is why the floor is one and not zero. The field is\n `repeated` because it shipped that way in v1 and renumbering a released\n field is not available to us, so the cardinality rides as a validation\n rule instead. `ResourceEntry.terms` is the plural side, bounded at 32: a\n resource carries many terms, an offer sells one of them.\n\nThe distinction is not cosmetic. `pricing` is what a Broker ranks on and\n what execute charges, so a term that is not on its own offer has no price\n a Broker can compare and no offer_id an agent can buy. Fusing several\n terms onto one offer makes every term but the first unsellable — a\n dual-licensed resource sells only under whichever arrangement the\n publisher happened to store first.\n\n The term here carries everything the publisher declared EXCEPT its price:\n its `pricing` MUST be unset (the offer.terms.pricing_unset rule), because\n the offer's price is `pricing` above. Its semantics, license,\n restrictions, quotas, obligations, scopes and part_label are the\n publisher's, unchanged.\n\n Where a term is reachable only under an existing subscription, its offer\n carries `subscription_id` and prices at zero marginal cost; the terms a\n requester may see at all are selected by `LicenseTerm.scopes`.\n\n See: Universal Licensing Core section.").optional(), "title": z.string().describe("Resource title (human-readable, for display/logging).").optional() }).describe("The FULL signed Offer for this batch entry, reflected back exactly as\n received at discovery. The Exchange verifies `offer.signature` over these\n presented bytes — stateless, no reconstruct-from-catalog. REQUIRED: every\n batch item carries its offer.") }).describe("TransactionItem — A single offer commitment within a batch transaction.")))(); +export const TransactionItemSchema = /* @__PURE__ */ (() => wire(z.object({ "agent_acceptance": z.object({ "signature": z.string().min(1).describe("Hex-encoded detached Ed25519 signature over the canonical AgentAcceptancePayload\n bytes (see the canonical-signing definition on Offer.signature)."), "signature_algorithm": z.string().describe("Signature algorithm; \"EdDSA\" for Ed25519.").default("") }).describe("The agent's detached acceptance signature over this item's `offer`.\n Optional on the wire; the Exchange enforces presence per\n item at the service layer for relayed batches. Signed bytes = the canonical\n AgentAcceptancePayload form, with requester_* and idempotency_key\n taken from the ENCLOSING TransactionRequest and offer_sig = offer.signature.").optional(), "offer": z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"fora.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("ResourceAttestation — Signed envelope of claims from a trusted party.\n\nA provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"fora.attestation.claims_schema\"].")).describe("Signed attestations about the resource at this URI.\n Attestations provide cryptographic proof of\n resource properties from trusted parties (providers or verification vendors).\n\nThree verification levels determine what is independently verifiable:\n Level 0 (no attestations): Resource may carry identifiers (DOI, IPTC GUID)\n for identification, but nothing is cryptographically verifiable.\n Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): Provider signs own claims with Ed25519 key.\n Agent can independently verify content hash and token count.\n CDN delivery failure + content hash mismatch are auto-disputable.\n Level 2 (third-party attested): Independent verification vendor crawled\n the resource and attested to its properties. Agent trusts the attestation\n (does not re-verify hash). Token count discrepancy is auto-disputable\n when corroborated by CDN response size.\n\n Multiple attestations may be present (e.g., provider self-attestation\n plus a third-party verification). Agents choose which to trust.").optional(), "data_as_of": z.string().datetime({ offset: true }).describe("When the offered data was current. For dynamic resources\n (resource_mutability = DYNAMIC), this is the snapshot timestamp.\n Enables the Broker to evaluate freshness: \"this credit report\n reflects data as of March 18\" or \"this drug database was updated today.\"\n\nNot set for STATIC resources (content doesn't change) or LIVE\n resources (content doesn't exist yet).\n\n The Broker compares this against RequestConstraints.max_data_age\n to filter stale offers. Example: agent requests max_data_age = 7 days,\n Broker drops offers where now() - data_as_of > 7 days.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource will be delivered.").default(0), "exchange": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("REQUIRED. Bare host of the Exchange that issued this offer (e.g.\n \"exchange.example\" or \"exchange.example:8081\"), in the form \"Request\n recipient\" defines in the file header. This is the execute-routing target:\n the agent, or a relaying Broker, sends the ExecuteTransaction call for this\n offer to this Exchange, and a Broker relaying a mixed batch groups the items\n by this value. Because it is an ordinary Offer field it falls inside the\n signed bytes (see `signature` below — the signature covers every field\n except `signature` / `signature_algorithm`), so an intermediary cannot\n redirect the execute call to a different Exchange without invalidating the\n offer, and it is what retires the X-FORA-Exchange-Endpoint transport header.\n It is also the audience statement of an ExecuteTransaction, which is why\n TransactionRequest carries no top-level `exchange`: on receipt, an Exchange\n MUST reject the request unless EVERY item's offer.exchange names its own\n domain. Presence is enforced because an empty value is unroutable — a\n relaying Broker has nothing to group or dial on, and the swap-protection\n above is vacuous when the signed bytes carry no recipient at all."), "expires_at": z.string().datetime({ offset: true }).describe("When this offer expires (ISO 8601). An offer for a term gated behind a\n verified delegation MUST NOT expire later than that delegation chain's\n effective expiry, the earliest `exp` in the chain (see Delegation).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "iab_categories": z.array(z.string()).describe("IAB Content Taxonomy category codes.\n Enables agents to filter offers by topic (e.g., \"only finance resources\").\n Uses IAB Content Taxonomy 3.1 codes.").optional(), "identity": z.object({ "c2pa_manifest": z.string().describe("C2PA content credentials manifest URI.\n Points to a sidecar or embedded C2PA manifest for this resource.\n C2PA-aware agents MAY follow this URI to validate the full provenance\n chain (creator identity, transformation history, ingredient composition)\n using C2PA libraries (JUMBF/COSE Sign1). C2PA-unaware agents can rely\n on c2pa_status and c2pa-bridged attestation claims instead.\n\nFormats:\n Sidecar: HTTPS URI to a .c2pa manifest file\n Embedded: same URI as canonical_url (manifest is inside the asset)\n Content Credentials Cloud: https://contentcredentials.org/verify?uri=...").optional(), "c2pa_status": z.enum(["C2PA_STATUS_TRUSTED","C2PA_STATUS_VALID","C2PA_STATUS_INVALID","C2PA_STATUS_ABSENT"]).describe("The full C2PA validation details (signer identity, trust list,\n action history, training/mining status) are carried in a\n ResourceAttestation with c2pa.* claims — see fora-c2pa-v1 profile.").optional(), "canonical_url": z.string().describe("Provider's authoritative URL for this resource (rel=\"canonical\").\n Always available. Different per provider for syndicated content.").optional(), "content_hash": z.string().describe("Hash of the content. Interpretation depends on hash_method:\n \"simhash-v1\" → locality-sensitive hash, for fuzzy dedup (Level 1)\n \"sha256\" → exact-match integrity hash (Level 2)\n\nLevel 1 (SimHash): computed by Exchange from extracted text.\n Agent verifies that fetched content is \"substantially similar.\"\n Tolerates dynamic page elements.\n\n Level 2 (SHA-256): computed by provider from deterministic payload.\n Agent verifies exact match. Requires provider to serve consistent\n content (e.g., API endpoint, static HTML, structured JSON).\n Mismatch = dispute. Commands premium pricing.").optional(), "doi": z.string().describe("Digital Object Identifier — persistent, never changes.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().describe("Hash algorithm and verification level.\n Examples: \"simhash-v1\", \"minhash-v1\", \"sha256\", \"sha384\"").optional(), "iptc_guid": z.string().describe("IPTC NewsML-G2 globally unique identifier.\n Present when resource flows through news wire syndication (AP, Reuters).").optional(), "isni": z.string().describe("International Standard Name Identifier for the creator.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Drives hash verification behavior:\n STATIC: content_hash is stable. Agent SHOULD verify delivered content matches.\n DYNAMIC: content changes between offer and fetch (credit reports, drug databases).\n content_hash reflects state at offer generation time. Hash mismatch is\n expected and MUST NOT trigger automatic dispute.\n LIVE: content does not exist at offer time (streaming feeds, live broadcasts).\n content_hash is not applicable. The \"resource\" is the stream endpoint.\n\n Validated across 18 use cases: static content (articles, patents, legislation),\n dynamic data (credit reports, drug interactions, stock snapshots), and live\n streams (MarketData quotes, NPR broadcast, news monitoring feeds)."), "soft_binding": z.string().describe("Soft binding hash — content-derived identifier that survives format\n transcoding (resolution changes, compression, PDF-to-text extraction).\n Extracted from C2PA soft binding assertion when present.\n Enables post-delivery verification when the hard binding hash breaks\n due to legitimate format conversion.\n\nAlgorithm specified in soft_binding_method. Values are algorithm-specific\n (e.g., perceptual hash hex string, watermark identifier).").optional(), "soft_binding_method": z.string().describe("Algorithm used for soft_binding.\n Examples: \"phash-v1\" (perceptual hash), \"c2pa-watermark\" (C2PA invisible\n watermark), \"chromaprint\" (audio fingerprint).").optional() }).describe("Resource identity for cross-exchange deduplication.\n Enables Brokers to recognize the same resource offered by\n different Exchanges and compare pricing.").optional(), "offer_id": z.string().describe("Unique identifier for this offer, assigned by the Exchange.\n Opaque to the caller: not derived from the resource, its URL, or any\n other field, and carries no meaning beyond identifying this offer.\n Two offers for the same resource have different offer_ids.").default(""), "previews": z.array(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("Preview — Lightweight resource preview for offer evaluation.\n\nThe Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no FORA transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")).describe("Lightweight previews for offer evaluation.\n The Exchange holds URLs (50–200 bytes each); the provider's CDN serves\n the actual bytes. Agents fetch previews only when evaluating offers —\n not on every discovery query. Multiple previews at different sizes\n allow agents to pick the cheapest fetch for their evaluation needs.\n\nPer content type:\n Image: watermarked thumbnail (150–450px JPEG)\n Video: short clip (10–30s MP4, watermarked)\n Audio: short clip (15–30s MP3, low-bitrate or watermarked)\n Text: snippet or abstract (first 200 words as text/plain)\n Data: sample records (1–3 rows as application/json)\n Stream: optional frame capture or none (streams are priced by time)\n\n Modeled after Shutterstock (multi-size thumbnail URLs),\n Spotify (preview_url to 30s clip), IIIF (parameterized image URLs),\n and OpenRTB native (img.url + dimensions).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("The offer's price, and its ONLY price. An offer represents a single\n licensing arrangement: each projected LicenseTerm yields its own offer, and\n the Exchange moves that term's price here, so the term inside `terms`\n carries no pricing of its own (the offer.terms.pricing_unset rule). This is\n the price execute charges, the price a Broker ranks and compares across\n Exchanges, and the price a metered purchase is charged at. A resource with\n multiple alternative terms (e.g. dual-licensed) produces multiple separate\n offers, one per term — never one offer with a \"headline\" picked among them.\n\nIt derives from exactly one catalog term: the term's pricing as the\n publisher declared it (ResourceEntry.terms). Each offer derives from one\n term, and the offer carries no second copy, so the offer's price and its\n term's price cannot disagree.\n\n On a metered (PER_UNIT) offer, the rate and the estimate a metered\n purchase is charged at are read from here (see Pricing). The estimate is\n optional. When the publisher states one — on the term's own pricing, or\n once for the resource on ResourceEntry.estimated_quantity — the Exchange\n carries it here as estimated_quantity, the term's own estimate taking\n precedence. When the publisher states none, the offer may carry none, and\n the purchase then charges one unit.").optional(), "reporting": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds). A report that arrives after\n the window closed is refused with USAGE_REPORT_REJECTION_REASON_WINDOW_EXPIRED\n (Connect code failed_precondition).").optional() }).describe("Post-usage reporting requirements for this offer.").optional(), "signature": z.string().describe("REQUIRED. Hex-encoded detached Ed25519 signature over the canonical\n serialization of the ENTIRE Offer — every field, including `pricing`,\n `terms` (the full licensing payload), `expires_at`, and `exchange`. Only\n `signature` and `signature_algorithm` are excluded from the signed bytes.\n `expires_at` is signed so the offer's validity window is\n integrity-protected: a relaying Broker cannot extend (or shorten) the TTL\n of a signed offer to replay it outside the window the Exchange intended.\n\nCANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n UNKNOWN FIELDS. A canonicalizer either OMITS content it has no schema for or\n PRESERVES it, and the rule follows from which:\n\n - OMITTING (e.g. proto-JSON, which emits only schema-defined fields): such a\n canonicalizer CANNOT reproduce the signed bytes of a message carrying\n unknown fields — what it renders silently drops part of what the signer\n covered. It MUST refuse the message rather than emit the reduced bytes,\n and a verifier built on it MUST reject rather than verify over them. The\n refusal binds at EVERY depth: a nested message and each element of a\n repeated or map field carries its own unknown-field set.\n - PRESERVING (a canonicalizer that carries unrecognized members through):\n it reproduces the signed bytes faithfully, so there is nothing to refuse.\n\n Either way an APPENDED field cannot pass: an omitting canonicalizer refuses\n the message, and a preserving one renders the appended member into bytes the\n signer never covered, so the signature fails. Without the refusal the omitting\n case would fail OPEN — an intermediary could add unknown fields to an\n already-signed message and leave its signature verifying, smuggling\n unauthenticated content through a message the recipient treats as verified.\n\n Extensions therefore ride in `ext` / `ext_critical`, which are defined fields\n and inside the signed bytes — never as undeclared field numbers.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.").default(""), "signature_algorithm": z.string().describe("JOSE/JWA algorithm identifier (RFC 8037 §3.1). Always 'EdDSA' for\n Ed25519. Advisory only: this field is cleared before the canonical\n payload is signed, so it is not covered by the signature.").default(""), "subscription_id": z.string().describe("If set, this offer is available under an existing subscription/deal.\n No per-request billing — usage tracked against subscription quota.\n Pricing.rate = \"0\" for subscription offers (zero marginal cost).\n The Broker SHOULD prefer subscription offers when available.").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("SubscriptionQuotaInfo — Proactive quota signaling for subscription access.\n\nAnalogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Subscription quota state, when this offer is under a subscription.\n Enables the agent to see remaining quota before committing.\n Multiple entries when the subscription has independent quotas\n (e.g., access count + spend cap).").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Obligation — A post-use behavioral requirement attached to a LicenseTerm.\n\nExamples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).max(64).describe("Post-use behavioral requirements.\n At most 64, for the reason quotas carries.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. Where it is required depends on the message that\n holds the term, so the requirement is a rule of that message rather than of\n this field:\n - On a catalog term (ResourceEntry.terms) it is REQUIRED regardless of\n semantics — an agent cannot act on a priceless term, so absent Pricing\n is a validation error at ingest (resource_entry.terms.pricing_required).\n model = FREE must be stated explicitly (absent Pricing is not free). A\n REFERENCE_ONLY term states its price here too; its License governs the\n human-readable terms but does not replace the machine-readable price.\n - On the term an offer carries (Offer.terms) it MUST be unset\n (offer.terms.pricing_unset): the offer's price is Offer.pricing, stated\n once.").optional(), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The unit being capped — an open vocabulary axis.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quota — A usage cap the term declares.\n\nA quota states how much a licensee may consume under the term before the\n term expires or must be renegotiated. Like a restriction, it is a declared\n term the parties agree to. Quotas are NOT billing quantities — billing is in\n Pricing.\n\n The metric vocabulary is authored ONLY in the (fora.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).max(64).describe("Usage caps. The agent must not exceed any individual Quota.\n At most 64, the bound every per-message list in this contract carries when\n no rule walks it more than once. It bounds what one term may carry, not the\n work of checking one — a validator walks every element it is handed before\n the cap is reported, so the cost of checking is bounded at the transport.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to. Defined-only: the axis set is\n CLOSED, and a number outside it is refused rather than ignored. A custom\n axis is RESTRICTION_KIND_OTHER, whose meaning rides in permitted/prohibited,\n so a new number was never the extension mechanism — accepting one would\n admit a restriction no consumer can evaluate onto a term whose default is\n BINDING (see advisory below), which fails open on the axis a publisher most\n needs honoured. Closing the axis does NOT bound the cost of the one-per-kind\n rule below, and must not be read as doing so: a number this rule refuses is\n still distinct from every other, so that rule's all() finds no duplicate to\n stop on and walks the list in full anyway. Its cost is bounded by the size\n test the rule itself carries."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restriction — A single constraint on one licensing dimension.\n\nRestrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: they are declared terms the parties agree to when the\n agent accepts the offer. The AGENT self-selects the term whose restrictions it\n can honour and bears compliance. The Exchange never enforces a restriction:\n it is NOT an Exchange-side gate the requester must pass to see or buy a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and by the scopes in the requester's signed\n request — see LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (fora.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — FORA user/organization categories")).max(8).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.\n At most 8, and this list is the one of the three that does NOT carry the\n contract's usual 64: only one restriction per axis is valid, the axis enum\n is defined-only, so four is the longest conformant list and eight leaves\n room for an axis this version does not have. Like the caps on quotas and\n obligations, this one bounds the DOCUMENT — how many restrictions one term\n may carry — and not the work of checking it: a validator walks every element\n it is handed before any cardinality rule is reported, so an over-cap list is\n traversed in full on its way to being refused.\n\nWhat makes this list different is that one rule walks it against ITSELF. The\n one-per-kind rule below is quadratic, so it carries its own size test and\n stays silent above this cap; a conformance guard holds the two numbers equal,\n because a cap raised without the test would leave the lists in between\n unchecked for duplicate axes and accepted. The neighbouring disjointness rule\n on each element is quadratic only in that element's two token lists, both\n capped at 64, so its cost is bounded per restriction and linear across the\n list — it needs no such test.").optional(), "scopes": z.array(z.string()).max(64).describe("Scope-gating: the Exchange presents an offer for this term only when the\n scopes in the requester's signed request (Requester.scopes) cover ALL of\n these scopes (AND-semantics). Empty = public. A subscription term is\n Pricing{model:FREE} + scopes:[\"subscription:...\"].\n\nRequester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n instead gate a term behind a verified delegation (a subscription term is\n the usual case): then only the delegation chain's verified effective scopes\n are matched against these scopes, and Requester.scopes is ignored for that\n term. To a requester that presents no valid delegation, it answers as if\n the term did not exist, as existence hiding requires (see\n Requester.scopes). An offer the Exchange presented is honoured until it\n expires, and a gated offer expires no later than the chain's effective\n expiry (see Delegation).\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments (at least one), and there is NO implicit prefix match\n (a grant narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("LicenseTerm — Universal licensing unit.\n\nOne LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation — with one move: on an offer the\n term's price is Offer.pricing, and the term itself carries none.\n\n Validation rules:\n - Pricing MUST be present on EVERY catalog term (ResourceEntry.terms),\n regardless of semantics — the resource_entry.terms.pricing_required\n rule. Absent Pricing → reject at ingest: an agent cannot act on a term\n with no price. This holds for REFERENCE_ONLY too — its License governs\n the human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - Pricing MUST be unset on the term an offer carries (Offer.terms) — the\n offer.terms.pricing_unset rule. The offer's price is Offer.pricing, and\n each offer derives from exactly one catalog term, so the price is stated\n once and cannot disagree with itself. The requirement is therefore a\n rule of the message that holds the term, not of LicenseTerm.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - A PER_UNIT term MAY state Pricing.estimated_quantity. It is not\n required, on the pushed term or on the offer it projects to — see\n Pricing.estimated_quantity.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).min(1).max(1).describe("The licensing term this offer sells, sourced from the publisher's\n ResourceEntry. EXACTLY ONE, and the bound is enforced rather than asked\n for: an offer IS a single licensing arrangement, so N terms on a resource\n project to N offers on that resource, each selling one of them — see\n `pricing` above. An offer with no term would be an offer projected from\n nothing, which is why the floor is one and not zero. The field is\n `repeated` because it shipped that way in v1 and renumbering a released\n field is not available to us, so the cardinality rides as a validation\n rule instead. `ResourceEntry.terms` is the plural side, bounded at 32: a\n resource carries many terms, an offer sells one of them.\n\nThe distinction is not cosmetic. `pricing` is what a Broker ranks on and\n what execute charges, so a term that is not on its own offer has no price\n a Broker can compare and no offer_id an agent can buy. Fusing several\n terms onto one offer makes every term but the first unsellable — a\n dual-licensed resource sells only under whichever arrangement the\n publisher happened to store first.\n\n The term here carries everything the publisher declared EXCEPT its price:\n its `pricing` MUST be unset (the offer.terms.pricing_unset rule), because\n the offer's price is `pricing` above. Its semantics, license,\n restrictions, quotas, obligations, scopes and part_label are the\n publisher's, unchanged.\n\n Where a term is reachable only under an existing subscription, its offer\n carries `subscription_id` and prices at zero marginal cost; the terms a\n requester may see at all are selected by `LicenseTerm.scopes`.\n\n See: Universal Licensing Core section.").optional(), "title": z.string().describe("Resource title (human-readable, for display/logging).").optional() }).describe("The FULL signed Offer for this batch entry, reflected back exactly as\n received at discovery. The Exchange verifies `offer.signature` over these\n presented bytes — stateless, no reconstruct-from-catalog. REQUIRED: every\n batch item carries its offer.") }).describe("TransactionItem — A single offer commitment within a batch transaction.")))(); -export const TransactionRequestSchema = /* @__PURE__ */ (() => wire(z.object({ "agent_request_acceptance": z.object({ "payload": z.object({ "idempotency_key": z.string().default(""), "items": z.array(z.object({ "exchange": z.string().min(1), "offer_sig": z.string().min(1) }).describe("AgentRequestAcceptanceItem is the minimum reference needed to authorize an\n offer's membership, order, and fan-out destination without repeating the\n full Offer in every projected subrequest. Offer.signature transitively binds\n the full offer, including its exchange field; the explicit exchange lets a\n recipient derive which signed references must appear in its projection.")).min(1).max(256).describe("Complete original request order, before Broker fan-out. Capped at 256 —\n the same ceiling a discovery query's uris list carries, so one request\n can reference at most one offer per queried URI at the query cap. The Go\n verification helper enforces the same bound itself before doing any\n canonicalization work, because a verifier may run with wire validation\n off and the canonical rendering of an unbounded list is the expensive\n step an unauthenticated caller could otherwise buy for free.").optional(), "requester_domain": z.string().describe("Requester.domain of the request. Non-empty (see AgentRequestAcceptance).").default(""), "requester_id": z.string().describe("Requester.id of the request. Non-empty (see AgentRequestAcceptance).").default("") }).describe("The signed payload is carried because a projected subrequest does not carry\n offers addressed to other Exchanges and therefore cannot reconstruct the\n original complete set by itself."), "signature": z.string().min(1).describe("Hex-encoded detached Ed25519 signature over the canonical payload bytes."), "signature_algorithm": z.string().describe("Signature algorithm; \"EdDSA\" for Ed25519.").default("") }).describe("Optional for wire compatibility. When present, an Exchange verifies this\n before creating or serving request-level idempotency state. A Broker MUST\n forward it unchanged on every projected subrequest. Older clients that omit\n it retain per-item execution semantics but receive no request-level claim.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "idempotency_key": z.string().min(1).max(255).describe("Idempotency key (REQUIRED). The server MUST dedupe on this: a replay returns\n the original result rather than re-executing, and the same key with\n different items is refused with already_exists. The key is scoped to the\n agent, whatever the route: a purchase repeated directly or through any\n Broker the Exchange admits is the same purchase, and the Broker forwards the\n agent's key unchanged. A key another agent or tenant used is never a\n collision. The full rule is \"Idempotency\" in the file header. The\n transaction's durable identity is the Exchange-assigned transaction_id in\n the response."), "items": z.array(z.object({ "agent_acceptance": z.object({ "signature": z.string().min(1).describe("Hex-encoded detached Ed25519 signature over the canonical AgentAcceptancePayload\n bytes (see the canonical-signing definition on Offer.signature)."), "signature_algorithm": z.string().describe("Signature algorithm; \"EdDSA\" for Ed25519.").default("") }).describe("The agent's detached acceptance signature over this item's `offer`.\n Optional on the wire; the Exchange enforces presence per\n item at the service layer for relayed batches. Signed bytes = the canonical\n AgentAcceptancePayload form, with requester_* and idempotency_key\n taken from the ENCLOSING TransactionRequest and offer_sig = offer.signature.").optional(), "offer": z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"fora.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("ResourceAttestation — Signed envelope of claims from a trusted party.\n\nA provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"fora.attestation.claims_schema\"].")).describe("Signed attestations about the resource at this URI.\n Attestations provide cryptographic proof of\n resource properties from trusted parties (providers or verification vendors).\n\nThree verification levels determine what is independently verifiable:\n Level 0 (no attestations): Resource may carry identifiers (DOI, IPTC GUID)\n for identification, but nothing is cryptographically verifiable.\n Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): Provider signs own claims with Ed25519 key.\n Agent can independently verify content hash and token count.\n CDN delivery failure + content hash mismatch are auto-disputable.\n Level 2 (third-party attested): Independent verification vendor crawled\n the resource and attested to its properties. Agent trusts the attestation\n (does not re-verify hash). Token count discrepancy is auto-disputable\n when corroborated by CDN response size.\n\n Multiple attestations may be present (e.g., provider self-attestation\n plus a third-party verification). Agents choose which to trust.").optional(), "data_as_of": z.string().datetime({ offset: true }).describe("When the offered data was current. For dynamic resources\n (resource_mutability = DYNAMIC), this is the snapshot timestamp.\n Enables the Broker to evaluate freshness: \"this credit report\n reflects data as of March 18\" or \"this drug database was updated today.\"\n\nNot set for STATIC resources (content doesn't change) or LIVE\n resources (content doesn't exist yet).\n\n The Broker compares this against RequestConstraints.max_data_age\n to filter stale offers. Example: agent requests max_data_age = 7 days,\n Broker drops offers where now() - data_as_of > 7 days.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource will be delivered.").default(0), "exchange": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("REQUIRED. Bare host of the Exchange that issued this offer (e.g.\n \"exchange.example\" or \"exchange.example:8081\"), in the form \"Request\n recipient\" defines in the file header. This is the execute-routing target:\n the agent, or a relaying Broker, sends the ExecuteTransaction call for this\n offer to this Exchange, and a Broker relaying a mixed batch groups the items\n by this value. Because it is an ordinary Offer field it falls inside the\n signed bytes (see `signature` below — the signature covers every field\n except `signature` / `signature_algorithm`), so an intermediary cannot\n redirect the execute call to a different Exchange without invalidating the\n offer, and it is what retires the X-FORA-Exchange-Endpoint transport header.\n It is also the audience statement of an ExecuteTransaction, which is why\n TransactionRequest carries no top-level `exchange`: on receipt, an Exchange\n MUST reject the request unless EVERY item's offer.exchange names its own\n domain. Presence is enforced because an empty value is unroutable — a\n relaying Broker has nothing to group or dial on, and the swap-protection\n above is vacuous when the signed bytes carry no recipient at all."), "expires_at": z.string().datetime({ offset: true }).describe("When this offer expires (ISO 8601).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "iab_categories": z.array(z.string()).describe("IAB Content Taxonomy category codes.\n Enables agents to filter offers by topic (e.g., \"only finance resources\").\n Uses IAB Content Taxonomy 3.1 codes.").optional(), "identity": z.object({ "c2pa_manifest": z.string().describe("C2PA content credentials manifest URI.\n Points to a sidecar or embedded C2PA manifest for this resource.\n C2PA-aware agents MAY follow this URI to validate the full provenance\n chain (creator identity, transformation history, ingredient composition)\n using C2PA libraries (JUMBF/COSE Sign1). C2PA-unaware agents can rely\n on c2pa_status and c2pa-bridged attestation claims instead.\n\nFormats:\n Sidecar: HTTPS URI to a .c2pa manifest file\n Embedded: same URI as canonical_url (manifest is inside the asset)\n Content Credentials Cloud: https://contentcredentials.org/verify?uri=...").optional(), "c2pa_status": z.enum(["C2PA_STATUS_TRUSTED","C2PA_STATUS_VALID","C2PA_STATUS_INVALID","C2PA_STATUS_ABSENT"]).describe("The full C2PA validation details (signer identity, trust list,\n action history, training/mining status) are carried in a\n ResourceAttestation with c2pa.* claims — see fora-c2pa-v1 profile.").optional(), "canonical_url": z.string().describe("Provider's authoritative URL for this resource (rel=\"canonical\").\n Always available. Different per provider for syndicated content.").optional(), "content_hash": z.string().describe("Hash of the content. Interpretation depends on hash_method:\n \"simhash-v1\" → locality-sensitive hash, for fuzzy dedup (Level 1)\n \"sha256\" → exact-match integrity hash (Level 2)\n\nLevel 1 (SimHash): computed by Exchange from extracted text.\n Agent verifies that fetched content is \"substantially similar.\"\n Tolerates dynamic page elements.\n\n Level 2 (SHA-256): computed by provider from deterministic payload.\n Agent verifies exact match. Requires provider to serve consistent\n content (e.g., API endpoint, static HTML, structured JSON).\n Mismatch = dispute. Commands premium pricing.").optional(), "doi": z.string().describe("Digital Object Identifier — persistent, never changes.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().describe("Hash algorithm and verification level.\n Examples: \"simhash-v1\", \"minhash-v1\", \"sha256\", \"sha384\"").optional(), "iptc_guid": z.string().describe("IPTC NewsML-G2 globally unique identifier.\n Present when resource flows through news wire syndication (AP, Reuters).").optional(), "isni": z.string().describe("International Standard Name Identifier for the creator.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Drives hash verification behavior:\n STATIC: content_hash is stable. Agent SHOULD verify delivered content matches.\n DYNAMIC: content changes between offer and fetch (credit reports, drug databases).\n content_hash reflects state at offer generation time. Hash mismatch is\n expected and MUST NOT trigger automatic dispute.\n LIVE: content does not exist at offer time (streaming feeds, live broadcasts).\n content_hash is not applicable. The \"resource\" is the stream endpoint.\n\n Validated across 18 use cases: static content (articles, patents, legislation),\n dynamic data (credit reports, drug interactions, stock snapshots), and live\n streams (MarketData quotes, NPR broadcast, news monitoring feeds)."), "soft_binding": z.string().describe("Soft binding hash — content-derived identifier that survives format\n transcoding (resolution changes, compression, PDF-to-text extraction).\n Extracted from C2PA soft binding assertion when present.\n Enables post-delivery verification when the hard binding hash breaks\n due to legitimate format conversion.\n\nAlgorithm specified in soft_binding_method. Values are algorithm-specific\n (e.g., perceptual hash hex string, watermark identifier).").optional(), "soft_binding_method": z.string().describe("Algorithm used for soft_binding.\n Examples: \"phash-v1\" (perceptual hash), \"c2pa-watermark\" (C2PA invisible\n watermark), \"chromaprint\" (audio fingerprint).").optional() }).describe("Resource identity for cross-exchange deduplication.\n Enables Brokers to recognize the same resource offered by\n different Exchanges and compare pricing.").optional(), "offer_id": z.string().describe("Unique identifier for this offer, assigned by the Exchange.\n Opaque to the caller: not derived from the resource, its URL, or any\n other field, and carries no meaning beyond identifying this offer.\n Two offers for the same resource have different offer_ids.").default(""), "previews": z.array(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("Preview — Lightweight resource preview for offer evaluation.\n\nThe Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no FORA transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")).describe("Lightweight previews for offer evaluation.\n The Exchange holds URLs (50–200 bytes each); the provider's CDN serves\n the actual bytes. Agents fetch previews only when evaluating offers —\n not on every discovery query. Multiple previews at different sizes\n allow agents to pick the cheapest fetch for their evaluation needs.\n\nPer content type:\n Image: watermarked thumbnail (150–450px JPEG)\n Video: short clip (10–30s MP4, watermarked)\n Audio: short clip (15–30s MP3, low-bitrate or watermarked)\n Text: snippet or abstract (first 200 words as text/plain)\n Data: sample records (1–3 rows as application/json)\n Stream: optional frame capture or none (streams are priced by time)\n\n Modeled after Shutterstock (multi-size thumbnail URLs),\n Spotify (preview_url to 30s clip), IIIF (parameterized image URLs),\n and OpenRTB native (img.url + dimensions).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("The offer's price, and its ONLY price. An offer represents a single\n licensing arrangement: each projected LicenseTerm yields its own offer, and\n the Exchange moves that term's price here, so the term inside `terms`\n carries no pricing of its own (the offer.terms.pricing_unset rule). This is\n the price execute charges, the price a Broker ranks and compares across\n Exchanges, and the price a metered purchase is charged at. A resource with\n multiple alternative terms (e.g. dual-licensed) produces multiple separate\n offers, one per term — never one offer with a \"headline\" picked among them.\n\nIt derives from exactly one catalog term: the term's pricing as the\n publisher declared it (ResourceEntry.terms). Each offer derives from one\n term, and the offer carries no second copy, so the offer's price and its\n term's price cannot disagree.\n\n On a metered (PER_UNIT) offer, the rate and the estimate a metered\n purchase is charged at are read from here (see Pricing). The estimate is\n optional. When the publisher states one — on the term's own pricing, or\n once for the resource on ResourceEntry.estimated_quantity — the Exchange\n carries it here as estimated_quantity, the term's own estimate taking\n precedence. When the publisher states none, the offer may carry none, and\n the purchase then charges one unit.").optional(), "reporting": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds). A report that arrives after\n the window closed is refused with USAGE_REPORT_REJECTION_REASON_WINDOW_EXPIRED\n (Connect code failed_precondition).").optional() }).describe("Post-usage reporting requirements for this offer.").optional(), "signature": z.string().describe("REQUIRED. Hex-encoded detached Ed25519 signature over the canonical\n serialization of the ENTIRE Offer — every field, including `pricing`,\n `terms` (the full licensing payload), `expires_at`, and `exchange`. Only\n `signature` and `signature_algorithm` are excluded from the signed bytes.\n `expires_at` is signed so the offer's validity window is\n integrity-protected: a relaying Broker cannot extend (or shorten) the TTL\n of a signed offer to replay it outside the window the Exchange intended.\n\nCANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n UNKNOWN FIELDS. A canonicalizer either OMITS content it has no schema for or\n PRESERVES it, and the rule follows from which:\n\n - OMITTING (e.g. proto-JSON, which emits only schema-defined fields): such a\n canonicalizer CANNOT reproduce the signed bytes of a message carrying\n unknown fields — what it renders silently drops part of what the signer\n covered. It MUST refuse the message rather than emit the reduced bytes,\n and a verifier built on it MUST reject rather than verify over them. The\n refusal binds at EVERY depth: a nested message and each element of a\n repeated or map field carries its own unknown-field set.\n - PRESERVING (a canonicalizer that carries unrecognized members through):\n it reproduces the signed bytes faithfully, so there is nothing to refuse.\n\n Either way an APPENDED field cannot pass: an omitting canonicalizer refuses\n the message, and a preserving one renders the appended member into bytes the\n signer never covered, so the signature fails. Without the refusal the omitting\n case would fail OPEN — an intermediary could add unknown fields to an\n already-signed message and leave its signature verifying, smuggling\n unauthenticated content through a message the recipient treats as verified.\n\n Extensions therefore ride in `ext` / `ext_critical`, which are defined fields\n and inside the signed bytes — never as undeclared field numbers.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.").default(""), "signature_algorithm": z.string().describe("JOSE/JWA algorithm identifier (RFC 8037 §3.1). Always 'EdDSA' for\n Ed25519. Advisory only: this field is cleared before the canonical\n payload is signed, so it is not covered by the signature.").default(""), "subscription_id": z.string().describe("If set, this offer is available under an existing subscription/deal.\n No per-request billing — usage tracked against subscription quota.\n Pricing.rate = \"0\" for subscription offers (zero marginal cost).\n The Broker SHOULD prefer subscription offers when available.").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("SubscriptionQuotaInfo — Proactive quota signaling for subscription access.\n\nAnalogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Subscription quota state, when this offer is under a subscription.\n Enables the agent to see remaining quota before committing.\n Multiple entries when the subscription has independent quotas\n (e.g., access count + spend cap).").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Obligation — A post-use behavioral requirement attached to a LicenseTerm.\n\nExamples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).max(64).describe("Post-use behavioral requirements.\n At most 64, for the reason quotas carries.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. Where it is required depends on the message that\n holds the term, so the requirement is a rule of that message rather than of\n this field:\n - On a catalog term (ResourceEntry.terms) it is REQUIRED regardless of\n semantics — an agent cannot act on a priceless term, so absent Pricing\n is a validation error at ingest (resource_entry.terms.pricing_required).\n model = FREE must be stated explicitly (absent Pricing is not free). A\n REFERENCE_ONLY term states its price here too; its License governs the\n human-readable terms but does not replace the machine-readable price.\n - On the term an offer carries (Offer.terms) it MUST be unset\n (offer.terms.pricing_unset): the offer's price is Offer.pricing, stated\n once.").optional(), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The unit being capped — an open vocabulary axis.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quota — A usage cap the term declares.\n\nA quota states how much a licensee may consume under the term before the\n term expires or must be renegotiated. Like a restriction, it is a declared\n term the parties agree to. Quotas are NOT billing quantities — billing is in\n Pricing.\n\n The metric vocabulary is authored ONLY in the (fora.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).max(64).describe("Usage caps. The agent must not exceed any individual Quota.\n At most 64, the bound every per-message list in this contract carries when\n no rule walks it more than once. It bounds what one term may carry, not the\n work of checking one — a validator walks every element it is handed before\n the cap is reported, so the cost of checking is bounded at the transport.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to. Defined-only: the axis set is\n CLOSED, and a number outside it is refused rather than ignored. A custom\n axis is RESTRICTION_KIND_OTHER, whose meaning rides in permitted/prohibited,\n so a new number was never the extension mechanism — accepting one would\n admit a restriction no consumer can evaluate onto a term whose default is\n BINDING (see advisory below), which fails open on the axis a publisher most\n needs honoured. Closing the axis does NOT bound the cost of the one-per-kind\n rule below, and must not be read as doing so: a number this rule refuses is\n still distinct from every other, so that rule's all() finds no duplicate to\n stop on and walks the list in full anyway. Its cost is bounded by the size\n test the rule itself carries."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restriction — A single constraint on one licensing dimension.\n\nRestrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: they are declared terms the parties agree to when the\n agent accepts the offer. The AGENT self-selects the term whose restrictions it\n can honour and bears compliance. The Exchange never enforces a restriction:\n it is NOT an Exchange-side gate the requester must pass to see or buy a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and by the scopes in the requester's signed\n request — see LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (fora.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — FORA user/organization categories")).max(8).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.\n At most 8, and this list is the one of the three that does NOT carry the\n contract's usual 64: only one restriction per axis is valid, the axis enum\n is defined-only, so four is the longest conformant list and eight leaves\n room for an axis this version does not have. Like the caps on quotas and\n obligations, this one bounds the DOCUMENT — how many restrictions one term\n may carry — and not the work of checking it: a validator walks every element\n it is handed before any cardinality rule is reported, so an over-cap list is\n traversed in full on its way to being refused.\n\nWhat makes this list different is that one rule walks it against ITSELF. The\n one-per-kind rule below is quadratic, so it carries its own size test and\n stays silent above this cap; a conformance guard holds the two numbers equal,\n because a cap raised without the test would leave the lists in between\n unchecked for duplicate axes and accepted. The neighbouring disjointness rule\n on each element is quadratic only in that element's two token lists, both\n capped at 64, so its cost is bounded per restriction and linear across the\n list — it needs no such test.").optional(), "scopes": z.array(z.string()).max(64).describe("Scope-gating: the Exchange presents an offer for this term only when the\n scopes in the requester's signed request (Requester.scopes) cover ALL of\n these scopes (AND-semantics). Empty = public. A subscription term is\n Pricing{model:FREE} + scopes:[\"subscription:...\"].\n\nRequester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n also require a verified delegation that grants these scopes before it\n presents the term. To a requester that presents none, it answers as if the\n term did not exist, as existence hiding requires (see Requester.scopes). An\n offer the Exchange presented is honoured until it expires.\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("LicenseTerm — Universal licensing unit.\n\nOne LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation — with one move: on an offer the\n term's price is Offer.pricing, and the term itself carries none.\n\n Validation rules:\n - Pricing MUST be present on EVERY catalog term (ResourceEntry.terms),\n regardless of semantics — the resource_entry.terms.pricing_required\n rule. Absent Pricing → reject at ingest: an agent cannot act on a term\n with no price. This holds for REFERENCE_ONLY too — its License governs\n the human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - Pricing MUST be unset on the term an offer carries (Offer.terms) — the\n offer.terms.pricing_unset rule. The offer's price is Offer.pricing, and\n each offer derives from exactly one catalog term, so the price is stated\n once and cannot disagree with itself. The requirement is therefore a\n rule of the message that holds the term, not of LicenseTerm.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - A PER_UNIT term MAY state Pricing.estimated_quantity. It is not\n required, on the pushed term or on the offer it projects to — see\n Pricing.estimated_quantity.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).min(1).max(1).describe("The licensing term this offer sells, sourced from the publisher's\n ResourceEntry. EXACTLY ONE, and the bound is enforced rather than asked\n for: an offer IS a single licensing arrangement, so N terms on a resource\n project to N offers on that resource, each selling one of them — see\n `pricing` above. An offer with no term would be an offer projected from\n nothing, which is why the floor is one and not zero. The field is\n `repeated` because it shipped that way in v1 and renumbering a released\n field is not available to us, so the cardinality rides as a validation\n rule instead. `ResourceEntry.terms` is the plural side, bounded at 32: a\n resource carries many terms, an offer sells one of them.\n\nThe distinction is not cosmetic. `pricing` is what a Broker ranks on and\n what execute charges, so a term that is not on its own offer has no price\n a Broker can compare and no offer_id an agent can buy. Fusing several\n terms onto one offer makes every term but the first unsellable — a\n dual-licensed resource sells only under whichever arrangement the\n publisher happened to store first.\n\n The term here carries everything the publisher declared EXCEPT its price:\n its `pricing` MUST be unset (the offer.terms.pricing_unset rule), because\n the offer's price is `pricing` above. Its semantics, license,\n restrictions, quotas, obligations, scopes and part_label are the\n publisher's, unchanged.\n\n Where a term is reachable only under an existing subscription, its offer\n carries `subscription_id` and prices at zero marginal cost; the terms a\n requester may see at all are selected by `LicenseTerm.scopes`.\n\n See: Universal Licensing Core section.").optional(), "title": z.string().describe("Resource title (human-readable, for display/logging).").optional() }).describe("The FULL signed Offer for this batch entry, reflected back exactly as\n received at discovery. The Exchange verifies `offer.signature` over these\n presented bytes — stateless, no reconstruct-from-catalog. REQUIRED: every\n batch item carries its offer.") }).describe("TransactionItem — A single offer commitment within a batch transaction.")).min(1).describe("The offers committed in this request (REQUIRED, min 1), each carrying its\n own reflected signed Offer + detached acceptance. A single offer is the\n degenerate 1-element list. The Exchange verifies each item's\n `offer.signature` (which covers pricing, terms, and expires_at) over the\n presented bytes against its own key — stateless, self-contained bearer\n tokens, with no reconstruct-from-catalog.").optional(), "requester": z.object({ "delegation": z.object({ "expires_at": z.string().datetime({ offset: true }).describe("When this delegation expires. Exchange MUST reject expired tokens.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "issuer": z.string().describe("Token issuer. OIDC issuer URL or GNAP grant server URL.\n Exchange uses this for JWT validation (OIDC discovery → JWKS)\n or GNAP token introspection.").optional(), "max_accesses": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Maximum number of accesses allowed under this delegation.\n Exchange tracks cumulative access count against this cap.\n Deny with DENIAL_REASON_QUOTA_EXCEEDED when count >= limit.\n For subscriptions with \"10,000 accesses/month\", this carries the ceiling.").optional(), "max_spend_cents": z.coerce.number().int().describe("Maximum spend in currency minor units (e.g., cents for USD).\n Exchange tracks cumulative spend against this cap.").optional(), "principal_domain": z.string().describe("Who granted this delegation (domain for public key lookup).").default(""), "principal_id": z.string().describe("Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").").default(""), "quota_period": z.string().describe("Quota reset period. How often the access/spend counters reset.\n Example: 30 days for monthly subscriptions — \"2592000s\" on the wire\n (proto-JSON encodes Duration as seconds; \"720h\" is not accepted).\n When absent, the quota is lifetime (bounded only by expires_at).").optional(), "revocation_uri": z.string().describe("Optional: URI for real-time revocation checking.\n Exchange MAY check this for high-value transactions.\n Not checked for routine low-value access (performance tradeoff).").optional(), "scopes": z.array(z.string()).describe("Scopes granted by this delegation. MUST be a subset of the\n principal's own scopes (attenuation — can only narrow, not widen).").optional(), "token": z.string().regex(new RegExp("^[A-Za-z0-9+/]*={0,2}$")).describe("Token bytes. A JWT (base64url-encoded JWS).").default(""), "token_format": z.string().describe("Token format: \"jwt\" (default). Empty is treated as \"jwt\". The field stays\n open for a future format.").default("") }).describe("Optional delegation — present when the requester acts on behalf of\n another entity (user, organization, upstream agent). On a query a Broker\n originated it is not honoured, because the Broker signs alone and nothing\n on that leg can pass its holder binding; the Exchange answers as if none\n were present (see Delegation).").optional(), "domain": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("REQUIRED. Bare host of the requester's key directory: the WBA directory at\n {domain}/.well-known/http-message-signatures-directory that publishes the\n agent's Ed25519 keys. It carries the bare-host shape \"Request recipient\"\n defines in the file header (a port allowed), for the same structural\n reason: a scheme, path or query smuggled in here would choose what gets\n fetched, not merely from where. It is never a free label. Every verifier\n reads it as the name of that directory, and the rule that binds it depends\n on whether the request is direct or relayed, which the receiver decides as\n \"Direct and relayed requests\" in the file header states:\n\n- Direct request. The agent's own RFC 9421 signature arrives because\n the agent sent the request itself. The verifier resolves the agent's\n keys from the COVERED `Signature-Agent` member, never from this field.\n It then MUST require this field to name that same directory: the host\n of the origin that member names, compared by\n the identity rule \"Request recipient\" defines (the shape check first,\n then case-folded, an absent port the same as \":443\", a subdomain a\n different party). A mismatch is refused as UNAUTHENTICATED with\n `request_auth_failure` SIGNATURE_INVALID: the signature verifies, but\n not for the requester the body names. A Broker applies this rule at\n BrokerService.ExecuteTransaction. At BrokerService.Resolve it refuses\n nothing for this field: every query it originates names the host of\n the origin that member names (see DiscoveryRequest.requester). An\n Exchange applies it on every request whose signer it does not admit as\n a Broker; a mismatch from a signer it does admit is a relayed request,\n one of the two cases below.\n - Purchase relayed through a Broker. BrokerService.ExecuteTransaction\n re-packages the purchase, so the request signature and the covered\n `Signature-Agent` member are the Broker's. They say only that the call comes\n from the Broker, and they sign no purchase. The Exchange MUST verify\n each item's AgentAcceptance, and the AgentRequestAcceptance, against\n the Ed25519 keys currently valid in the directory this field names\n (see AgentRequestAcceptance), never against the Broker's key. Those\n acceptances are the only agent signatures the Exchange sees, and the\n purchase belongs to the account of the directory they verify under\n (see \"Accounts\" in the file header).\n - Broker-led discovery. A ResourceQuery a Broker originated carries no agent\n signature, so nothing on that leg authenticates this field: it is the\n Broker's statement, under the Broker's own signature, of whom it\n queries for. A delegation on such a query is not honoured (see\n Delegation)."), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "id": z.string().min(1).max(255).describe("REQUIRED. A free label the agent chooses for attribution (e.g.\n \"agent-research-bot-001\"), 1 to 255 characters. Use it to tell apart the\n sub-agents or end customers that share one key directory. It is never\n identity: no verifier finds keys from it, and no party trusts it for an\n authentication or authorization decision. The requester's identity is\n `domain` together with the key that verifies. The acceptance signatures\n cover this label (see AgentAcceptancePayload), so a relaying Broker cannot\n rewrite it, but its value is still only what the agent says."), "name": z.string().describe("Human-readable name (e.g., \"Acme Research Assistant\").").optional(), "scopes": z.array(z.string()).max(64).describe("Entitlement scopes: the requester's own statement of what it is entitled\n to. On a query a Broker originated, they are the Broker's statement for the\n agent. They are signed and attributable, and the Exchange does not verify\n them. Scope coverage decides which terms the Exchange presents (see\n LicenseTerm.scopes, which also states when an Exchange MAY require a\n verified delegation); it is not access control. A false statement is\n traceable to the signing key, its directory and its account, and the\n agent's acceptance signs the scoped offer it buys; it is settled through\n reconciliation and the account relationship.\n\nEXISTENCE HIDING. A requester never learns about a resource outside its\n scopes. When the scopes leave no presentable offer for a resource, the\n Exchange answers with no offers and no absence reason (an OfferGroup with\n empty `offers` and `absence_reason` unset), exactly as for any resource\n with nothing to offer. A Broker relaying that answer adds no reason of its\n own. At purchase, an offer the Exchange presented is honoured until it\n expires, so a scope refusal cannot arise there. No reason in the protocol\n reports a scope shortfall: OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT and\n DENIAL_REASON_SCOPE_INSUFFICIENT are deprecated and never sent.\n\n Scope format: colon-separated segments, \"{domain}:{permission}\" or\n \"{profile}:{permission}\", optionally multi-segment (\"dist:US:CA\");\n matching is segment-wise per the rule below (no implicit hierarchy).\n Examples:\n \"credit:read\" — can access credit reports\n \"subscription:marketdata-2026\" — has active MarketData subscription\n \"academic:*\" — full access to academic resources\n \"internal:reports\" — can access internal reports\n \"*\" — unrestricted (public Exchange default)\n\n Matching is SEGMENT-WISE (\":\" separated). A granted scope G covers a\n required scope R iff, segment by segment, each G segment equals the\n corresponding R segment or is \"*\"; a terminal \"*\" matches all remaining\n segments. There is NO implicit prefix match, and a grant NARROWER than\n the requirement does not cover it (G must be equal-to-or-broader than R).\n Examples: \"dist:*\" covers \"dist:US\" and \"dist:US:CA\"; \"dist:US:*\" covers\n \"dist:US:CA\" but not \"dist:EU\"; bare \"dist\" covers only \"dist\"; granted\n \"dist:US:CA\" does NOT cover required \"dist:US\"; \"*\" covers everything.\n This same rule governs LicenseTerm.scopes — one algorithm protocol-wide.\n\n When empty, Exchange applies its default access policy (typically\n returns all publicly available resources).").optional(), "type": z.enum(["REQUESTER_TYPE_AGENT","REQUESTER_TYPE_HUMAN_TOOL","REQUESTER_TYPE_SERVICE","REQUESTER_TYPE_DELEGATED","REQUESTER_TYPE_RESEARCH"]).describe("What kind of entity is making this request.") }).describe("Requester identity — forwarded for authorization and audit. REQUIRED: a\n purchase names its requester, whose Requester.id and Requester.domain are\n required in turn, and every acceptance binds that requester."), "ver": z.string().describe("FORA protocol version — \"1.0\". Stamped by the sender from a single\n constant; advisory on receive. See \"Protocol version\" in the file header.").default("") }).describe("TransactionRequest — Commit to one or more offers.\n\nAfter selecting offers, the caller commits by sending this to the\n Exchange. Supports both single-offer and batch (multi-offer) modes.\n The Exchange validates eligibility, authorizes billing, creates\n delivery, and logs each transaction.")))(); +export const TransactionRequestSchema = /* @__PURE__ */ (() => wire(z.object({ "agent_request_acceptance": z.object({ "payload": z.object({ "idempotency_key": z.string().default(""), "items": z.array(z.object({ "exchange": z.string().min(1), "offer_sig": z.string().min(1) }).describe("AgentRequestAcceptanceItem is the minimum reference needed to authorize an\n offer's membership, order, and fan-out destination without repeating the\n full Offer in every projected subrequest. Offer.signature transitively binds\n the full offer, including its exchange field; the explicit exchange lets a\n recipient derive which signed references must appear in its projection.")).min(1).max(256).describe("Complete original request order, before Broker fan-out. Capped at 256 —\n the same ceiling a discovery query's uris list carries, so one request\n can reference at most one offer per queried URI at the query cap. The Go\n verification helper enforces the same bound itself before doing any\n canonicalization work, because a verifier may run with wire validation\n off and the canonical rendering of an unbounded list is the expensive\n step an unauthenticated caller could otherwise buy for free.").optional(), "requester_domain": z.string().describe("Requester.domain of the request. Non-empty (see AgentRequestAcceptance).").default(""), "requester_id": z.string().describe("Requester.id of the request. Non-empty (see AgentRequestAcceptance).").default("") }).describe("The signed payload is carried because a projected subrequest does not carry\n offers addressed to other Exchanges and therefore cannot reconstruct the\n original complete set by itself."), "signature": z.string().min(1).describe("Hex-encoded detached Ed25519 signature over the canonical payload bytes."), "signature_algorithm": z.string().describe("Signature algorithm; \"EdDSA\" for Ed25519.").default("") }).describe("Optional for wire compatibility. When present, an Exchange verifies this\n before creating or serving request-level idempotency state. A Broker MUST\n forward it unchanged on every projected subrequest. Older clients that omit\n it retain per-item execution semantics but receive no request-level claim.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "idempotency_key": z.string().min(1).max(255).describe("Idempotency key (REQUIRED). The server MUST dedupe on this: a replay returns\n the original result rather than re-executing, and the same key with\n different items is refused with already_exists. The key is scoped to the\n agent, whatever the route: a purchase repeated directly or through any\n Broker the Exchange admits is the same purchase, and the Broker forwards the\n agent's key unchanged. A key another agent or tenant used is never a\n collision. The full rule is \"Idempotency\" in the file header. The\n transaction's durable identity is the Exchange-assigned transaction_id in\n the response."), "items": z.array(z.object({ "agent_acceptance": z.object({ "signature": z.string().min(1).describe("Hex-encoded detached Ed25519 signature over the canonical AgentAcceptancePayload\n bytes (see the canonical-signing definition on Offer.signature)."), "signature_algorithm": z.string().describe("Signature algorithm; \"EdDSA\" for Ed25519.").default("") }).describe("The agent's detached acceptance signature over this item's `offer`.\n Optional on the wire; the Exchange enforces presence per\n item at the service layer for relayed batches. Signed bytes = the canonical\n AgentAcceptancePayload form, with requester_* and idempotency_key\n taken from the ENCLOSING TransactionRequest and offer_sig = offer.signature.").optional(), "offer": z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"fora.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("ResourceAttestation — Signed envelope of claims from a trusted party.\n\nA provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"fora.attestation.claims_schema\"].")).describe("Signed attestations about the resource at this URI.\n Attestations provide cryptographic proof of\n resource properties from trusted parties (providers or verification vendors).\n\nThree verification levels determine what is independently verifiable:\n Level 0 (no attestations): Resource may carry identifiers (DOI, IPTC GUID)\n for identification, but nothing is cryptographically verifiable.\n Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): Provider signs own claims with Ed25519 key.\n Agent can independently verify content hash and token count.\n CDN delivery failure + content hash mismatch are auto-disputable.\n Level 2 (third-party attested): Independent verification vendor crawled\n the resource and attested to its properties. Agent trusts the attestation\n (does not re-verify hash). Token count discrepancy is auto-disputable\n when corroborated by CDN response size.\n\n Multiple attestations may be present (e.g., provider self-attestation\n plus a third-party verification). Agents choose which to trust.").optional(), "data_as_of": z.string().datetime({ offset: true }).describe("When the offered data was current. For dynamic resources\n (resource_mutability = DYNAMIC), this is the snapshot timestamp.\n Enables the Broker to evaluate freshness: \"this credit report\n reflects data as of March 18\" or \"this drug database was updated today.\"\n\nNot set for STATIC resources (content doesn't change) or LIVE\n resources (content doesn't exist yet).\n\n The Broker compares this against RequestConstraints.max_data_age\n to filter stale offers. Example: agent requests max_data_age = 7 days,\n Broker drops offers where now() - data_as_of > 7 days.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource will be delivered.").default(0), "exchange": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("REQUIRED. Bare host of the Exchange that issued this offer (e.g.\n \"exchange.example\" or \"exchange.example:8081\"), in the form \"Request\n recipient\" defines in the file header. This is the execute-routing target:\n the agent, or a relaying Broker, sends the ExecuteTransaction call for this\n offer to this Exchange, and a Broker relaying a mixed batch groups the items\n by this value. Because it is an ordinary Offer field it falls inside the\n signed bytes (see `signature` below — the signature covers every field\n except `signature` / `signature_algorithm`), so an intermediary cannot\n redirect the execute call to a different Exchange without invalidating the\n offer, and it is what retires the X-FORA-Exchange-Endpoint transport header.\n It is also the audience statement of an ExecuteTransaction, which is why\n TransactionRequest carries no top-level `exchange`: on receipt, an Exchange\n MUST reject the request unless EVERY item's offer.exchange names its own\n domain. Presence is enforced because an empty value is unroutable — a\n relaying Broker has nothing to group or dial on, and the swap-protection\n above is vacuous when the signed bytes carry no recipient at all."), "expires_at": z.string().datetime({ offset: true }).describe("When this offer expires (ISO 8601). An offer for a term gated behind a\n verified delegation MUST NOT expire later than that delegation chain's\n effective expiry, the earliest `exp` in the chain (see Delegation).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "iab_categories": z.array(z.string()).describe("IAB Content Taxonomy category codes.\n Enables agents to filter offers by topic (e.g., \"only finance resources\").\n Uses IAB Content Taxonomy 3.1 codes.").optional(), "identity": z.object({ "c2pa_manifest": z.string().describe("C2PA content credentials manifest URI.\n Points to a sidecar or embedded C2PA manifest for this resource.\n C2PA-aware agents MAY follow this URI to validate the full provenance\n chain (creator identity, transformation history, ingredient composition)\n using C2PA libraries (JUMBF/COSE Sign1). C2PA-unaware agents can rely\n on c2pa_status and c2pa-bridged attestation claims instead.\n\nFormats:\n Sidecar: HTTPS URI to a .c2pa manifest file\n Embedded: same URI as canonical_url (manifest is inside the asset)\n Content Credentials Cloud: https://contentcredentials.org/verify?uri=...").optional(), "c2pa_status": z.enum(["C2PA_STATUS_TRUSTED","C2PA_STATUS_VALID","C2PA_STATUS_INVALID","C2PA_STATUS_ABSENT"]).describe("The full C2PA validation details (signer identity, trust list,\n action history, training/mining status) are carried in a\n ResourceAttestation with c2pa.* claims — see fora-c2pa-v1 profile.").optional(), "canonical_url": z.string().describe("Provider's authoritative URL for this resource (rel=\"canonical\").\n Always available. Different per provider for syndicated content.").optional(), "content_hash": z.string().describe("Hash of the content. Interpretation depends on hash_method:\n \"simhash-v1\" → locality-sensitive hash, for fuzzy dedup (Level 1)\n \"sha256\" → exact-match integrity hash (Level 2)\n\nLevel 1 (SimHash): computed by Exchange from extracted text.\n Agent verifies that fetched content is \"substantially similar.\"\n Tolerates dynamic page elements.\n\n Level 2 (SHA-256): computed by provider from deterministic payload.\n Agent verifies exact match. Requires provider to serve consistent\n content (e.g., API endpoint, static HTML, structured JSON).\n Mismatch = dispute. Commands premium pricing.").optional(), "doi": z.string().describe("Digital Object Identifier — persistent, never changes.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().describe("Hash algorithm and verification level.\n Examples: \"simhash-v1\", \"minhash-v1\", \"sha256\", \"sha384\"").optional(), "iptc_guid": z.string().describe("IPTC NewsML-G2 globally unique identifier.\n Present when resource flows through news wire syndication (AP, Reuters).").optional(), "isni": z.string().describe("International Standard Name Identifier for the creator.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Drives hash verification behavior:\n STATIC: content_hash is stable. Agent SHOULD verify delivered content matches.\n DYNAMIC: content changes between offer and fetch (credit reports, drug databases).\n content_hash reflects state at offer generation time. Hash mismatch is\n expected and MUST NOT trigger automatic dispute.\n LIVE: content does not exist at offer time (streaming feeds, live broadcasts).\n content_hash is not applicable. The \"resource\" is the stream endpoint.\n\n Validated across 18 use cases: static content (articles, patents, legislation),\n dynamic data (credit reports, drug interactions, stock snapshots), and live\n streams (MarketData quotes, NPR broadcast, news monitoring feeds)."), "soft_binding": z.string().describe("Soft binding hash — content-derived identifier that survives format\n transcoding (resolution changes, compression, PDF-to-text extraction).\n Extracted from C2PA soft binding assertion when present.\n Enables post-delivery verification when the hard binding hash breaks\n due to legitimate format conversion.\n\nAlgorithm specified in soft_binding_method. Values are algorithm-specific\n (e.g., perceptual hash hex string, watermark identifier).").optional(), "soft_binding_method": z.string().describe("Algorithm used for soft_binding.\n Examples: \"phash-v1\" (perceptual hash), \"c2pa-watermark\" (C2PA invisible\n watermark), \"chromaprint\" (audio fingerprint).").optional() }).describe("Resource identity for cross-exchange deduplication.\n Enables Brokers to recognize the same resource offered by\n different Exchanges and compare pricing.").optional(), "offer_id": z.string().describe("Unique identifier for this offer, assigned by the Exchange.\n Opaque to the caller: not derived from the resource, its URL, or any\n other field, and carries no meaning beyond identifying this offer.\n Two offers for the same resource have different offer_ids.").default(""), "previews": z.array(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("Preview — Lightweight resource preview for offer evaluation.\n\nThe Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no FORA transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")).describe("Lightweight previews for offer evaluation.\n The Exchange holds URLs (50–200 bytes each); the provider's CDN serves\n the actual bytes. Agents fetch previews only when evaluating offers —\n not on every discovery query. Multiple previews at different sizes\n allow agents to pick the cheapest fetch for their evaluation needs.\n\nPer content type:\n Image: watermarked thumbnail (150–450px JPEG)\n Video: short clip (10–30s MP4, watermarked)\n Audio: short clip (15–30s MP3, low-bitrate or watermarked)\n Text: snippet or abstract (first 200 words as text/plain)\n Data: sample records (1–3 rows as application/json)\n Stream: optional frame capture or none (streams are priced by time)\n\n Modeled after Shutterstock (multi-size thumbnail URLs),\n Spotify (preview_url to 30s clip), IIIF (parameterized image URLs),\n and OpenRTB native (img.url + dimensions).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("The offer's price, and its ONLY price. An offer represents a single\n licensing arrangement: each projected LicenseTerm yields its own offer, and\n the Exchange moves that term's price here, so the term inside `terms`\n carries no pricing of its own (the offer.terms.pricing_unset rule). This is\n the price execute charges, the price a Broker ranks and compares across\n Exchanges, and the price a metered purchase is charged at. A resource with\n multiple alternative terms (e.g. dual-licensed) produces multiple separate\n offers, one per term — never one offer with a \"headline\" picked among them.\n\nIt derives from exactly one catalog term: the term's pricing as the\n publisher declared it (ResourceEntry.terms). Each offer derives from one\n term, and the offer carries no second copy, so the offer's price and its\n term's price cannot disagree.\n\n On a metered (PER_UNIT) offer, the rate and the estimate a metered\n purchase is charged at are read from here (see Pricing). The estimate is\n optional. When the publisher states one — on the term's own pricing, or\n once for the resource on ResourceEntry.estimated_quantity — the Exchange\n carries it here as estimated_quantity, the term's own estimate taking\n precedence. When the publisher states none, the offer may carry none, and\n the purchase then charges one unit.").optional(), "reporting": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds). A report that arrives after\n the window closed is refused with USAGE_REPORT_REJECTION_REASON_WINDOW_EXPIRED\n (Connect code failed_precondition).").optional() }).describe("Post-usage reporting requirements for this offer.").optional(), "signature": z.string().describe("REQUIRED. Hex-encoded detached Ed25519 signature over the canonical\n serialization of the ENTIRE Offer — every field, including `pricing`,\n `terms` (the full licensing payload), `expires_at`, and `exchange`. Only\n `signature` and `signature_algorithm` are excluded from the signed bytes.\n `expires_at` is signed so the offer's validity window is\n integrity-protected: a relaying Broker cannot extend (or shorten) the TTL\n of a signed offer to replay it outside the window the Exchange intended.\n\nCANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n UNKNOWN FIELDS. A canonicalizer either OMITS content it has no schema for or\n PRESERVES it, and the rule follows from which:\n\n - OMITTING (e.g. proto-JSON, which emits only schema-defined fields): such a\n canonicalizer CANNOT reproduce the signed bytes of a message carrying\n unknown fields — what it renders silently drops part of what the signer\n covered. It MUST refuse the message rather than emit the reduced bytes,\n and a verifier built on it MUST reject rather than verify over them. The\n refusal binds at EVERY depth: a nested message and each element of a\n repeated or map field carries its own unknown-field set.\n - PRESERVING (a canonicalizer that carries unrecognized members through):\n it reproduces the signed bytes faithfully, so there is nothing to refuse.\n\n Either way an APPENDED field cannot pass: an omitting canonicalizer refuses\n the message, and a preserving one renders the appended member into bytes the\n signer never covered, so the signature fails. Without the refusal the omitting\n case would fail OPEN — an intermediary could add unknown fields to an\n already-signed message and leave its signature verifying, smuggling\n unauthenticated content through a message the recipient treats as verified.\n\n Extensions therefore ride in `ext` / `ext_critical`, which are defined fields\n and inside the signed bytes — never as undeclared field numbers.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.").default(""), "signature_algorithm": z.string().describe("JOSE/JWA algorithm identifier (RFC 8037 §3.1). Always 'EdDSA' for\n Ed25519. Advisory only: this field is cleared before the canonical\n payload is signed, so it is not covered by the signature.").default(""), "subscription_id": z.string().describe("If set, this offer is available under an existing subscription/deal.\n No per-request billing — usage tracked against subscription quota.\n Pricing.rate = \"0\" for subscription offers (zero marginal cost).\n The Broker SHOULD prefer subscription offers when available.").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("SubscriptionQuotaInfo — Proactive quota signaling for subscription access.\n\nAnalogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Subscription quota state, when this offer is under a subscription.\n Enables the agent to see remaining quota before committing.\n Multiple entries when the subscription has independent quotas\n (e.g., access count + spend cap).").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("Canonical identity of the license document (RFC 3986). MUST NOT be\n URL-validated — data-labels TDL identifiers use non-URL schemes.\n For REFERENCE_ONLY terms this is the authoritative specification.\n Examples:\n \"https://creativecommons.org/licenses/by/4.0/\"\n \"https://techcrunch.com/licensing/ai-terms-2026\"\n\n\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("Cryptographic digest of the document at `uri`, in \"method:hexdigest\" form\n (e.g. \"sha256:9f86d081...\"). Pins the referenced document so a consumer can\n verify the bytes it fetches match what was offered; covered by the offer\n signature, so it is tamper-evident end to end. REQUIRED whenever `uri` is\n non-empty — any semantics, mutable or not: without a pinned digest a MitM\n (or the publisher) can swap the document the agent reads. The Exchange pins\n it at ingestion (computing it over the safely-fetched document, or\n accepting a publisher-supplied value when uri is not HTTP-fetchable, e.g. a\n non-URL TDL scheme).\n\nThe method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Obligation — A post-use behavioral requirement attached to a LicenseTerm.\n\nExamples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).max(64).describe("Post-use behavioral requirements.\n At most 64, for the reason quotas carries.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit (`unit`).\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.\n\nOPTIONAL everywhere: on a LicenseTerm a publisher pushes and on an Offer.\n It is E in the metered price rule above. When a metered offer states it,\n it is positive (the offer.metered.estimate_positive rule), and the\n purchase charges E × rate. When a metered offer states none, the purchase\n charges one unit, 1 × rate. A PER_UNIT price may state a rate\n with or without an estimate; whether to state one is the publisher's\n decision, and nothing requires it.\n\n The estimate describes the resource rather than the arrangement, so a\n publisher may state it once on ResourceEntry.estimated_quantity instead of\n on each term. An estimate the publisher states either way is carried onto\n the offer's pricing (see Offer.pricing).").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("Metering basis — the \"per what\" of PER_UNIT pricing. REQUIRED when\n model = PER_UNIT. Custom units namespace as \"vendor:unit\". Ignored for\n FREE / FLAT.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. Where it is required depends on the message that\n holds the term, so the requirement is a rule of that message rather than of\n this field:\n - On a catalog term (ResourceEntry.terms) it is REQUIRED regardless of\n semantics — an agent cannot act on a priceless term, so absent Pricing\n is a validation error at ingest (resource_entry.terms.pricing_required).\n model = FREE must be stated explicitly (absent Pricing is not free). A\n REFERENCE_ONLY term states its price here too; its License governs the\n human-readable terms but does not replace the machine-readable price.\n - On the term an offer carries (Offer.terms) it MUST be unset\n (offer.terms.pricing_unset): the offer's price is Offer.pricing, stated\n once.").optional(), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The unit being capped — an open vocabulary axis.\n\nThe (fora.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quota — A usage cap the term declares.\n\nA quota states how much a licensee may consume under the term before the\n term expires or must be renegotiated. Like a restriction, it is a declared\n term the parties agree to. Quotas are NOT billing quantities — billing is in\n Pricing.\n\n The metric vocabulary is authored ONLY in the (fora.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).max(64).describe("Usage caps. The agent must not exceed any individual Quota.\n At most 64, the bound every per-message list in this contract carries when\n no rule walks it more than once. It bounds what one term may carry, not the\n work of checking one — a validator walks every element it is handed before\n the cap is reported, so the cost of checking is bounded at the transport.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to. Defined-only: the axis set is\n CLOSED, and a number outside it is refused rather than ignored. A custom\n axis is RESTRICTION_KIND_OTHER, whose meaning rides in permitted/prohibited,\n so a new number was never the extension mechanism — accepting one would\n admit a restriction no consumer can evaluate onto a term whose default is\n BINDING (see advisory below), which fails open on the axis a publisher most\n needs honoured. Closing the axis does NOT bound the cost of the one-per-kind\n rule below, and must not be read as doing so: a number this rule refuses is\n still distinct from every other, so that rule's all() finds no duplicate to\n stop on and walks the list in full anyway. Its cost is bounded by the size\n test the rule itself carries."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restriction — A single constraint on one licensing dimension.\n\nRestrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: they are declared terms the parties agree to when the\n agent accepts the offer. The AGENT self-selects the term whose restrictions it\n can honour and bears compliance. The Exchange never enforces a restriction:\n it is NOT an Exchange-side gate the requester must pass to see or buy a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and by the scopes in the requester's signed\n request — see LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (fora.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — FORA user/organization categories")).max(8).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.\n At most 8, and this list is the one of the three that does NOT carry the\n contract's usual 64: only one restriction per axis is valid, the axis enum\n is defined-only, so four is the longest conformant list and eight leaves\n room for an axis this version does not have. Like the caps on quotas and\n obligations, this one bounds the DOCUMENT — how many restrictions one term\n may carry — and not the work of checking it: a validator walks every element\n it is handed before any cardinality rule is reported, so an over-cap list is\n traversed in full on its way to being refused.\n\nWhat makes this list different is that one rule walks it against ITSELF. The\n one-per-kind rule below is quadratic, so it carries its own size test and\n stays silent above this cap; a conformance guard holds the two numbers equal,\n because a cap raised without the test would leave the lists in between\n unchecked for duplicate axes and accepted. The neighbouring disjointness rule\n on each element is quadratic only in that element's two token lists, both\n capped at 64, so its cost is bounded per restriction and linear across the\n list — it needs no such test.").optional(), "scopes": z.array(z.string()).max(64).describe("Scope-gating: the Exchange presents an offer for this term only when the\n scopes in the requester's signed request (Requester.scopes) cover ALL of\n these scopes (AND-semantics). Empty = public. A subscription term is\n Pricing{model:FREE} + scopes:[\"subscription:...\"].\n\nRequester.scopes is the requester's own statement, which the Exchange does\n not verify, so a scope gate hides a term from requesters that do not state\n the scope; it does not stop one that states it falsely. An Exchange MAY\n instead gate a term behind a verified delegation (a subscription term is\n the usual case): then only the delegation chain's verified effective scopes\n are matched against these scopes, and Requester.scopes is ignored for that\n term. To a requester that presents no valid delegation, it answers as if\n the term did not exist, as existence hiding requires (see\n Requester.scopes). An offer the Exchange presented is honoured until it\n expires, and a gated offer expires no later than the chain's effective\n expiry (see Delegation).\n\n Coverage uses the SAME matching rule as Requester.scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments (at least one), and there is NO implicit prefix match\n (a grant narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("LicenseTerm — Universal licensing unit.\n\nOne LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation — with one move: on an offer the\n term's price is Offer.pricing, and the term itself carries none.\n\n Validation rules:\n - Pricing MUST be present on EVERY catalog term (ResourceEntry.terms),\n regardless of semantics — the resource_entry.terms.pricing_required\n rule. Absent Pricing → reject at ingest: an agent cannot act on a term\n with no price. This holds for REFERENCE_ONLY too — its License governs\n the human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - Pricing MUST be unset on the term an offer carries (Offer.terms) — the\n offer.terms.pricing_unset rule. The offer's price is Offer.pricing, and\n each offer derives from exactly one catalog term, so the price is stated\n once and cannot disagree with itself. The requirement is therefore a\n rule of the message that holds the term, not of LicenseTerm.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - A PER_UNIT term MAY state Pricing.estimated_quantity. It is not\n required, on the pushed term or on the offer it projects to — see\n Pricing.estimated_quantity.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).min(1).max(1).describe("The licensing term this offer sells, sourced from the publisher's\n ResourceEntry. EXACTLY ONE, and the bound is enforced rather than asked\n for: an offer IS a single licensing arrangement, so N terms on a resource\n project to N offers on that resource, each selling one of them — see\n `pricing` above. An offer with no term would be an offer projected from\n nothing, which is why the floor is one and not zero. The field is\n `repeated` because it shipped that way in v1 and renumbering a released\n field is not available to us, so the cardinality rides as a validation\n rule instead. `ResourceEntry.terms` is the plural side, bounded at 32: a\n resource carries many terms, an offer sells one of them.\n\nThe distinction is not cosmetic. `pricing` is what a Broker ranks on and\n what execute charges, so a term that is not on its own offer has no price\n a Broker can compare and no offer_id an agent can buy. Fusing several\n terms onto one offer makes every term but the first unsellable — a\n dual-licensed resource sells only under whichever arrangement the\n publisher happened to store first.\n\n The term here carries everything the publisher declared EXCEPT its price:\n its `pricing` MUST be unset (the offer.terms.pricing_unset rule), because\n the offer's price is `pricing` above. Its semantics, license,\n restrictions, quotas, obligations, scopes and part_label are the\n publisher's, unchanged.\n\n Where a term is reachable only under an existing subscription, its offer\n carries `subscription_id` and prices at zero marginal cost; the terms a\n requester may see at all are selected by `LicenseTerm.scopes`.\n\n See: Universal Licensing Core section.").optional(), "title": z.string().describe("Resource title (human-readable, for display/logging).").optional() }).describe("The FULL signed Offer for this batch entry, reflected back exactly as\n received at discovery. The Exchange verifies `offer.signature` over these\n presented bytes — stateless, no reconstruct-from-catalog. REQUIRED: every\n batch item carries its offer.") }).describe("TransactionItem — A single offer commitment within a batch transaction.")).min(1).describe("The offers committed in this request (REQUIRED, min 1), each carrying its\n own reflected signed Offer + detached acceptance. A single offer is the\n degenerate 1-element list. The Exchange verifies each item's\n `offer.signature` (which covers pricing, terms, and expires_at) over the\n presented bytes against its own key — stateless, self-contained bearer\n tokens, with no reconstruct-from-catalog.").optional(), "requester": z.object({ "delegation": z.object({ "expires_at": z.string().datetime({ offset: true }).describe("Mirror of the chain's earliest `exp`. The Exchange MUST reject an expired\n token; the signed `exp` values decide, not this field.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "issuer": z.string().describe("Mirror of the ROOT token's `iss`, the identity configured with the trusted\n key (never an intermediate or terminal issuer). Informational: the\n Exchange never discovers a key from it, through OIDC, JWKS or otherwise.").optional(), "max_accesses": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Maximum number of accesses allowed under this delegation.\n Exchange tracks cumulative access count against this cap.\n Deny with DENIAL_REASON_QUOTA_EXCEEDED when count >= limit.\n For subscriptions with \"10,000 accesses/month\", this carries the ceiling.").optional(), "max_spend_cents": z.coerce.number().int().describe("Maximum spend in currency minor units (e.g., cents for USD).\n Exchange tracks cumulative spend against this cap.").optional(), "principal_domain": z.string().describe("Mirror of the ROOT token's `iss`, the configured trust anchor's identity.\n Informational: no key is looked up from it, and it establishes no trust.").default(""), "principal_id": z.string().describe("Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").\n Informational; not read by the verifier.").default(""), "quota_period": z.string().describe("Quota reset period. How often the access/spend counters reset.\n Example: 30 days for monthly subscriptions — \"2592000s\" on the wire\n (proto-JSON encodes Duration as seconds; \"720h\" is not accepted).\n When absent, the quota is lifetime (bounded only by expires_at).").optional(), "revocation_uri": z.string().describe("Informational. Revocation lookup is not part of the delegation profile:\n the verifier never fetches this URI. A chain is invalidated early by\n withdrawing the configured root key.").optional(), "scopes": z.array(z.string()).describe("Mirror of the terminal token's effective scopes. In the chain, each\n child's scopes are covered by its parent's under the scope-matching rule\n on Requester.scopes (attenuation — can only narrow, not widen).").optional(), "token": z.string().regex(new RegExp("^[A-Za-z0-9+/]{0,43691}={0,2}$")).max(43692).describe("Token bytes. A JWT (base64url-encoded JWS). At most 32768 bytes: the\n complete chain, every embedded parent included, must fit. The limit is a\n wire rule, checked before the token is parsed and independent of whether\n the bytes are a valid JWT; a longer token is an invalid request, not a\n per-item denial.").default(""), "token_format": z.string().describe("Token format: \"jwt\" (default). Empty is treated as \"jwt\". The field stays\n open for a future format.").default("") }).describe("Optional delegation — present when the requester acts on behalf of\n another entity (user, organization, upstream agent). On a query a Broker\n originated it is not honoured, because the Broker signs alone and nothing\n on that leg can pass its holder binding; the Exchange answers as if none\n were present (see Delegation).").optional(), "domain": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("REQUIRED. Bare host of the requester's key directory: the WBA directory at\n {domain}/.well-known/http-message-signatures-directory that publishes the\n agent's Ed25519 keys. It carries the bare-host shape \"Request recipient\"\n defines in the file header (a port allowed), for the same structural\n reason: a scheme, path or query smuggled in here would choose what gets\n fetched, not merely from where. It is never a free label. Every verifier\n reads it as the name of that directory, and the rule that binds it depends\n on whether the request is direct or relayed, which the receiver decides as\n \"Direct and relayed requests\" in the file header states:\n\n- Direct request. The agent's own RFC 9421 signature arrives because\n the agent sent the request itself. The verifier resolves the agent's\n keys from the COVERED `Signature-Agent` member, never from this field.\n It then MUST require this field to name that same directory: the host\n of the origin that member names, compared by\n the identity rule \"Request recipient\" defines (the shape check first,\n then case-folded, an absent port the same as \":443\", a subdomain a\n different party). A mismatch is refused as UNAUTHENTICATED with\n `request_auth_failure` SIGNATURE_INVALID: the signature verifies, but\n not for the requester the body names. A Broker applies this rule at\n BrokerService.ExecuteTransaction. At BrokerService.Resolve it refuses\n nothing for this field: every query it originates names the host of\n the origin that member names (see DiscoveryRequest.requester). An\n Exchange applies it on every request whose signer it does not admit as\n a Broker; a mismatch from a signer it does admit is a relayed request,\n one of the two cases below.\n - Purchase relayed through a Broker. BrokerService.ExecuteTransaction\n re-packages the purchase, so the request signature and the covered\n `Signature-Agent` member are the Broker's. They say only that the call comes\n from the Broker, and they sign no purchase. The Exchange MUST verify\n each item's AgentAcceptance, and the AgentRequestAcceptance, against\n the Ed25519 keys currently valid in the directory this field names\n (see AgentRequestAcceptance), never against the Broker's key. Those\n acceptances are the only agent signatures the Exchange sees, and the\n purchase belongs to the account of the directory they verify under\n (see \"Accounts\" in the file header).\n - Broker-led discovery. A ResourceQuery a Broker originated carries no agent\n signature, so nothing on that leg authenticates this field: it is the\n Broker's statement, under the Broker's own signature, of whom it\n queries for. A delegation on such a query is not honoured (see\n Delegation)."), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "id": z.string().min(1).max(255).describe("REQUIRED. A free label the agent chooses for attribution (e.g.\n \"agent-research-bot-001\"), 1 to 255 characters. Use it to tell apart the\n sub-agents or end customers that share one key directory. It is never\n identity: no verifier finds keys from it, and no party trusts it for an\n authentication or authorization decision. The requester's identity is\n `domain` together with the key that verifies. The acceptance signatures\n cover this label (see AgentAcceptancePayload), so a relaying Broker cannot\n rewrite it, but its value is still only what the agent says."), "name": z.string().describe("Human-readable name (e.g., \"Acme Research Assistant\").").optional(), "scopes": z.array(z.string()).max(64).describe("Entitlement scopes: the requester's own statement of what it is entitled\n to. On a query a Broker originated, they are the Broker's statement for the\n agent. They are signed and attributable, and the Exchange does not verify\n them. Scope coverage decides which terms the Exchange presents (see\n LicenseTerm.scopes, which also states when an Exchange MAY require a\n verified delegation); it is not access control. A false statement is\n traceable to the signing key, its directory and its account, and the\n agent's acceptance signs the scoped offer it buys; it is settled through\n reconciliation and the account relationship.\n\nEXISTENCE HIDING. A requester never learns about a resource outside its\n scopes. When the scopes leave no presentable offer for a resource, the\n Exchange answers with no offers and no absence reason (an OfferGroup with\n empty `offers` and `absence_reason` unset), exactly as for any resource\n with nothing to offer. A Broker relaying that answer adds no reason of its\n own. At purchase, an offer the Exchange presented is honoured until it\n expires, so a scope refusal cannot arise there. No reason in the protocol\n reports a scope shortfall: OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT and\n DENIAL_REASON_SCOPE_INSUFFICIENT are deprecated and never sent.\n\n Scope format: colon-separated segments, \"{domain}:{permission}\" or\n \"{profile}:{permission}\", optionally multi-segment (\"dist:US:CA\");\n matching is segment-wise per the rule below (no implicit hierarchy).\n Examples:\n \"credit:read\" — can access credit reports\n \"subscription:marketdata-2026\" — has active MarketData subscription\n \"academic:*\" — full access to academic resources\n \"internal:reports\" — can access internal reports\n \"*\" — unrestricted (public Exchange default)\n\n Matching is SEGMENT-WISE (\":\" separated). A granted scope G covers a\n required scope R iff, segment by segment, each G segment equals the\n corresponding R segment or is \"*\"; a terminal \"*\" matches all remaining\n segments, of which there must be at least one. There is NO implicit prefix\n match, and a grant NARROWER than the requirement does not cover it (G must\n be equal-to-or-broader than R), so a grant with more segments than the\n requirement never covers it, even when its last segment is \"*\". A \"*\" in R\n is a literal segment: a narrow grant never covers a wildcard requirement.\n Matching is case-sensitive and never trims. Examples: \"dist:*\" covers\n \"dist:US\", \"dist:US:CA\" and \"dist:US:*\" but not \"dist\"; \"dist:US:*\" covers\n \"dist:US:CA\" but not \"dist:EU\" or \"dist:US\"; \"publisher:*:read\" covers\n \"publisher:news:read\" but not \"publisher:news:*\"; bare \"dist\" covers only\n \"dist\"; granted \"dist:US:CA\" does NOT cover required \"dist:US\"; \"*\" covers\n everything. This same rule governs LicenseTerm.scopes and the attenuation\n of a delegation child against its parent — one algorithm protocol-wide.\n\n When empty, Exchange applies its default access policy (typically\n returns all publicly available resources).").optional(), "type": z.enum(["REQUESTER_TYPE_AGENT","REQUESTER_TYPE_HUMAN_TOOL","REQUESTER_TYPE_SERVICE","REQUESTER_TYPE_DELEGATED","REQUESTER_TYPE_RESEARCH"]).describe("What kind of entity is making this request.") }).describe("Requester identity — forwarded for authorization and audit. REQUIRED: a\n purchase names its requester, whose Requester.id and Requester.domain are\n required in turn, and every acceptance binds that requester."), "ver": z.string().describe("FORA protocol version — \"1.0\". Stamped by the sender from a single\n constant; advisory on receive. See \"Protocol version\" in the file header.").default("") }).describe("TransactionRequest — Commit to one or more offers.\n\nAfter selecting offers, the caller commits by sending this to the\n Exchange. Supports both single-offer and batch (multi-offer) modes.\n The Exchange validates eligibility, authorizes billing, creates\n delivery, and logs each transaction.")))(); export const TransactionResponseSchema = /* @__PURE__ */ (() => wire(z.object({ "agent_identity_hash": z.string().describe("Identity that a delivered retrieval_endpoint is bound to: the RFC 7638 JWK\n Thumbprint of the agent's Ed25519 request-signing key (see \"Retrieval-URL\n identity binding\" above). On a sub-request a Broker re-packaged, the request\n signer is the Broker, and this is the thumbprint of the agent key the\n AgentAcceptances verify under instead. Shared across the request; set once.").default(""), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "items": z.array(z.object({ "billing_id": z.string().describe("Billing record identifier minted by the Exchange's billing adapter for\n this transaction (not the account handle — see RegisterResponse.billing_ref).").default(""), "cost": z.object({ "amount": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Exact decimal string (not a float), e.g. \"19.99\". Denominated in `currency`.").default(""), "currency": z.string().default(""), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).optional() }).describe("Cost for this item. For a metered (PER_UNIT) item this is the purchase\n charge: the offer's estimated_quantity × rate, or one unit, 1 × rate, when\n the offer states no estimate (see Pricing).").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource is delivered for this item.").default(0), "denial_reason": z.enum(["DENIAL_REASON_ACCOUNT_INACTIVE","DENIAL_REASON_INSUFFICIENT_BALANCE","DENIAL_REASON_RATE_LIMITED","DENIAL_REASON_CONTENT_UNAVAILABLE","DENIAL_REASON_RESTRICTION_NOT_SATISFIED","DENIAL_REASON_REPORTING_OVERDUE","DENIAL_REASON_OFFER_EXPIRED","DENIAL_REASON_SIGNATURE_INVALID","DENIAL_REASON_QUOTA_EXCEEDED","DENIAL_REASON_DELEGATION_INVALID","DENIAL_REASON_SCOPE_INSUFFICIENT","DENIAL_REASON_ENTITLEMENT_MISSING","DENIAL_REASON_ENTITLEMENT_MALFORMED","DENIAL_REASON_ENTITLEMENT_EXPIRED","DENIAL_REASON_ENTITLEMENT_WRONG_BUYER","DENIAL_REASON_SUBSCRIPTION_LAPSED","DENIAL_REASON_ENTITLEMENT_NOT_GRANTED","DENIAL_REASON_ACCOUNT_NOT_REGISTERED","DENIAL_REASON_RELAY_NOT_ACCEPTED","DENIAL_REASON_UNKNOWN_CRITICAL_EXTENSION"]).describe("Set if this specific item was denied (others may succeed). Every per-item\n denial is answered here, in a successful response, also when the request\n carried only this item: a denied one-item purchase is never a non-OK error.").optional(), "expires_at": z.string().datetime({ offset: true }).describe("When retrieval_endpoint expires.").optional(), "offer_id": z.string().describe("The offer_id this result is for.").default(""), "refusal": z.object({ "code": z.string().min(1).max(64).describe("The Connect code the party answered with, in its wire form, e.g.\n \"permission_denied\" or \"unauthenticated\". When the party gave no answer\n — unreachable, or the call timed out — it is the code the relaying party's\n call failed with (\"unavailable\", \"deadline_exceeded\"). A refusal with\n either of those codes is an ambiguous outcome: a purchase may have\n completed. On a Broker purchase, retrying the agent's request with the same\n idempotency_key, on any route, is answered from each Exchange's stored\n result, so it settles the outcome without buying twice."), "detail": z.object({ "catalog_rejection": z.object({ "reason": z.enum(["CATALOG_REJECTION_REASON_NOT_CATALOG_CONTRIBUTOR","CATALOG_REJECTION_REASON_TENANT_MISMATCH","CATALOG_REJECTION_REASON_DOMAIN_NOT_VERIFIED","CATALOG_REJECTION_REASON_SIGNATURE_INVALID","CATALOG_REJECTION_REASON_MALFORMED_ENTRY","CATALOG_REJECTION_REASON_UNKNOWN_VOCAB_TOKEN","CATALOG_REJECTION_REASON_QUOTA_EXCEEDED","CATALOG_REJECTION_REASON_TERMS_LIMIT_EXCEEDED","CATALOG_REJECTION_REASON_URI_UNAVAILABLE","CATALOG_REJECTION_REASON_UNKNOWN_CRITICAL_EXTENSION","CATALOG_REJECTION_REASON_EXCHANGE_NOT_LISTED"]).describe("The rejection reason (defined-only, non-zero)"), "rejected_paths": z.array(z.string()).describe("The entry paths the refusal is about. A catalog push is all-or-nothing, so\n these name which entries failed inside a submission that persisted nothing —\n they are not a list of what was dropped from an otherwise applied batch.").optional() }).describe("`reason` oneof — CatalogService rejection").optional(), "dispute_failure": z.object({ "reason": z.enum(["DISPUTE_FAILURE_REASON_TRANSACTION_NOT_FOUND","DISPUTE_FAILURE_REASON_REPORT_NOT_FILED","DISPUTE_FAILURE_REASON_WINDOW_EXPIRED","DISPUTE_FAILURE_REASON_DUPLICATE","DISPUTE_FAILURE_REASON_INELIGIBLE","DISPUTE_FAILURE_REASON_UNKNOWN_CRITICAL_EXTENSION"]).describe("The failure reason (defined-only, non-zero)") }).describe("`reason` oneof — DisputeTransaction filing refused").optional(), "domain": z.string().describe("Stable grouping for the failing surface, e.g. \"fora.v1.ExchangeService\".\n Mirrors google.rpc.ErrorInfo.domain so generic tooling can group errors.").default(""), "domain_verification_failure": z.object({ "reason": z.enum(["DOMAIN_VERIFICATION_FAILURE_REASON_CHALLENGE_NOT_FOUND","DOMAIN_VERIFICATION_FAILURE_REASON_CHALLENGE_MISMATCH","DOMAIN_VERIFICATION_FAILURE_REASON_CHALLENGE_EXPIRED","DOMAIN_VERIFICATION_FAILURE_REASON_FETCH_FAILED","DOMAIN_VERIFICATION_FAILURE_REASON_EXCHANGE_NOT_AUTHORIZED","DOMAIN_VERIFICATION_FAILURE_REASON_KEY_REGISTRATION_FAILED","DOMAIN_VERIFICATION_FAILURE_REASON_UNKNOWN_CRITICAL_EXTENSION"]).describe("The failure reason (defined-only, non-zero)") }).describe("`reason` oneof — domain verification failed").optional(), "message": z.string().describe("Developer-facing, NON-authoritative human message. Clients MUST branch on\n the typed reason below, never on this text. Servers SHOULD NOT place secrets,\n PII, or existence/authorization detail here that the closed typed reason\n deliberately withholds: unlike the enum, this free text is unbounded and\n easily becomes an existence oracle or leak channel (see `metadata`).").default(""), "metadata": z.record(z.string(), z.string()).describe("Dynamic key/value context that also appears in `message` (ids, limits,\n axes). Mirrors google.rpc.ErrorInfo.metadata. Strongly-typed context rides\n in the per-domain reason block below instead. Same leakage rule as `message`:\n servers SHOULD NOT put secrets, PII, or withheld existence/authorization\n detail here — it is the same potential side channel as the absence oracle.").optional(), "registration_failure": z.object({ "field_errors": z.array(z.object({ "error": z.string().min(1).max(255).describe("Developer-facing, NON-authoritative description of what failed\n (e.g. \"required\", \"must match ^[A-Z]{2}[0-9]+$\"). Wording is\n validator-defined and not stable across Exchanges; clients branch on\n `reason`, never on this text. States the constraint, NEVER the submitted\n value — the ErrorDetail leakage rule applies here too."), "path": z.string().max(255).describe("RFC 6901 JSON Pointer to the offending member, relative to\n registration_data (e.g. \"/vat_id\", \"/address/postal_code\"). The empty\n string addresses registration_data itself, for whole-object failures\n (oneOf, minProperties) that belong to no single member.").default("") }).describe("RegistrationFieldError — one registration_data member that failed the\n Exchange's published schema (AccountRegistration.data_schema).")).max(64).describe("When reason = INVALID_REGISTRATION_DATA: the registration_data members\n that are missing or do not conform. Empty for every other reason — enforced\n by the message rule above, not left to prose.").optional(), "reason": z.enum(["REGISTRATION_FAILURE_REASON_DOMAIN_NOT_VERIFIED","REGISTRATION_FAILURE_REASON_INVALID_KEY","REGISTRATION_FAILURE_REASON_SIGNATURE_INVALID","REGISTRATION_FAILURE_REASON_ALREADY_REGISTERED","REGISTRATION_FAILURE_REASON_QUOTA_EXCEEDED","REGISTRATION_FAILURE_REASON_INVALID_REGISTRATION_DATA","REGISTRATION_FAILURE_REASON_TERMS_DIGEST_STALE","REGISTRATION_FAILURE_REASON_UNKNOWN_CRITICAL_EXTENSION"]).describe("The failure reason (defined-only, non-zero)") }).describe("`reason` oneof — agent/provider registration refused").optional(), "request_auth_failure": z.object({ "reason": z.enum(["REQUEST_AUTH_FAILURE_REASON_SIGNATURE_MISSING","REQUEST_AUTH_FAILURE_REASON_SIGNATURE_INVALID","REQUEST_AUTH_FAILURE_REASON_SIGNATURE_STALE"]).describe("The failure reason (defined-only, non-zero)") }).describe("`reason` oneof — RFC 9421 request signature on an RPC refused").optional(), "retrieval_auth_failure": z.object({ "reason": z.enum(["RETRIEVAL_AUTH_FAILURE_REASON_URL_EXPIRED","RETRIEVAL_AUTH_FAILURE_REASON_URL_SIGNATURE_MISSING","RETRIEVAL_AUTH_FAILURE_REASON_URL_EXPIRY_MISSING","RETRIEVAL_AUTH_FAILURE_REASON_URL_SIGNATURE_MISMATCH","RETRIEVAL_AUTH_FAILURE_REASON_AGENT_KEY_MISSING","RETRIEVAL_AUTH_FAILURE_REASON_PROOF_SIGNATURE_MISSING","RETRIEVAL_AUTH_FAILURE_REASON_KEYID_MISMATCH","RETRIEVAL_AUTH_FAILURE_REASON_THUMBPRINT_MISMATCH","RETRIEVAL_AUTH_FAILURE_REASON_PROOF_CREATED_MISSING","RETRIEVAL_AUTH_FAILURE_REASON_PROOF_EXPIRY_MISSING","RETRIEVAL_AUTH_FAILURE_REASON_PROOF_EXPIRED","RETRIEVAL_AUTH_FAILURE_REASON_PROOF_SIGNATURE_INVALID"]).describe("The failure reason (defined-only, non-zero)") }).describe("`reason` oneof — signed-URL / proof-of-possession check failed").optional(), "transaction_denial": z.object({ "exchange": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("Bare host of the Exchange that PRODUCED this denial, in the form \"Request\n recipient\" defines in the file header. Not an echo of what the caller sent:\n on a relayed or fanned-out execute the request went to a Broker, so the\n Exchange that refused may not be one the agent named (through\n BrokerService.ExecuteTransaction this detail reaches the agent inside\n UpstreamRefusal.detail, beside UpstreamRefusal.party). Carrying it here is\n what lets ACCOUNT_NOT_REGISTERED be actionable — the agent learns where to\n call Register without fetching a manifest to work it out. NOTHING SIGNS THIS\n VALUE: it rides in a response, and on a relayed path the response passed\n through an intermediary, so this field is exactly the unsigned addressing\n the request-side `exchange` field exists to refuse. Treat it as a HINT, not\n an instruction. Before acting on it — and registering is a consequential act,\n handing an operator's business data and a signed acceptance of that\n Exchange's terms to whoever answers — a caller MUST check the value against\n a domain it already trusts for this transaction: the signed `offer.exchange`\n of the items in the refused request, or its own RequestConstraints.exchanges\n set. A value matching neither is reported to the caller and never dialled,\n because a hostile intermediary that could choose it would be choosing where\n an unattended agent registers.").optional(), "offer_id": z.string().describe("DEPRECATED, never set. A whole-request refusal pertains to no one offer; a\n denial of one offer rides on that offer's TransactionResultItem. The field\n is retained because removing it would break the v1 wire contract; a\n receiver ignores it.").optional(), "reason": z.enum(["DENIAL_REASON_ACCOUNT_INACTIVE","DENIAL_REASON_INSUFFICIENT_BALANCE","DENIAL_REASON_RATE_LIMITED","DENIAL_REASON_CONTENT_UNAVAILABLE","DENIAL_REASON_RESTRICTION_NOT_SATISFIED","DENIAL_REASON_REPORTING_OVERDUE","DENIAL_REASON_OFFER_EXPIRED","DENIAL_REASON_SIGNATURE_INVALID","DENIAL_REASON_QUOTA_EXCEEDED","DENIAL_REASON_DELEGATION_INVALID","DENIAL_REASON_SCOPE_INSUFFICIENT","DENIAL_REASON_ENTITLEMENT_MISSING","DENIAL_REASON_ENTITLEMENT_MALFORMED","DENIAL_REASON_ENTITLEMENT_EXPIRED","DENIAL_REASON_ENTITLEMENT_WRONG_BUYER","DENIAL_REASON_SUBSCRIPTION_LAPSED","DENIAL_REASON_ENTITLEMENT_NOT_GRANTED","DENIAL_REASON_ACCOUNT_NOT_REGISTERED","DENIAL_REASON_RELAY_NOT_ACCEPTED","DENIAL_REASON_UNKNOWN_CRITICAL_EXTENSION"]).describe("The denial reason (defined-only, non-zero)"), "restriction_mismatches": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("DEPRECATED, never set. RESTRICTION_NOT_SATISFIED is never sent: the\n Exchange never enforces a restriction (see Restriction). The field is\n retained because removing it would break the v1 wire contract; a receiver\n ignores it.").optional() }).describe("`reason` oneof — ExecuteTransaction refused as a whole request").optional(), "usage_report_rejection": z.object({ "reason": z.enum(["USAGE_REPORT_REJECTION_REASON_TRANSACTION_NOT_FOUND","USAGE_REPORT_REJECTION_REASON_DUPLICATE","USAGE_REPORT_REJECTION_REASON_WINDOW_EXPIRED","USAGE_REPORT_REJECTION_REASON_MISSING_REQUIRED_FIELDS","USAGE_REPORT_REJECTION_REASON_MALFORMED","USAGE_REPORT_REJECTION_REASON_UNKNOWN_CRITICAL_EXTENSION"]).describe("The rejection reason (defined-only, non-zero)") }).describe("`reason` oneof — ReportUsage filing rejected").optional() }).describe("The party's typed reason, unchanged — the ErrorDetail it attached to its\n refusal. Absent when the party attached none or gave no answer.").optional(), "party": z.string().regex(new RegExp("^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*(:(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3}))?$")).max(260).describe("Bare host of the party that refused the call made on the caller's behalf,\n in the form \"Request recipient\" defines in the file header.") }).describe("Set only on a BrokerTransactionResponse, and only when the Exchange that\n owns this item refused the Broker's whole sub-request instead of answering\n it: a non-OK answer, or no answer at all. offer_id names the item;\n transaction_id, billing_id, cost, denial_reason, retrieval_endpoint and the\n other result fields stay unset. Every item the Broker sent in that\n sub-request carries the same refusal. Whether the item was purchased\n depends on the refusal's code. With the code \"unavailable\" or\n \"deadline_exceeded\" the outcome is unknown: the Exchange may have completed\n the purchase before the answer was lost, whether it gave no answer or\n answered that code, and retrying the agent's request with the same\n idempotency_key settles it, on any route: through the same Broker, another\n one, or directly (see UpstreamRefusal.code). With any other code the Exchange answered with a\n refusal that decided no item, so the item was not purchased. An Exchange\n never sets this field, and a Broker never replaces it with a denial_reason:\n an upstream refusal is not a per-item access decision.").optional(), "reporting_obligation": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds). A report that arrives after\n the window closed is refused with USAGE_REPORT_REJECTION_REASON_WINDOW_EXPIRED\n (Connect code failed_precondition).").optional() }).describe("Reporting requirements for this item.").optional(), "resource_title": z.string().describe("Resource title echoed from the Offer.").optional(), "restriction_mismatches": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("DEPRECATED, never set. It named the restriction axes a request failed under\n DENIAL_REASON_RESTRICTION_NOT_SATISFIED, which is never sent: the Exchange\n never enforces a restriction (see Restriction). The field is retained\n because removing it would break the v1 wire contract; a receiver ignores it.").optional(), "retrieval_endpoint": z.string().describe("Signed retrieval URL for this item. Bound to the requesting agent's identity\n via the parent TransactionResponse.agent_identity_hash (shared across all\n batch items) — on a BrokerTransactionResponse, the agent_identity_hash of\n the ExchangeOutcome for this item's Exchange; expires at expires_at. Absent if this item was denied or its\n delivery_method is not signed-URL-based.").optional(), "subscription_id": z.string().describe("If under subscription, no per-request charge.").optional(), "subscription_unit_value": z.object({ "amount": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Exact decimal string (not a float), e.g. \"19.99\". Denominated in `currency`.").default(""), "currency": z.string().default(""), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).optional() }).describe("Computed per-unit cost for financial attribution on subscription transactions.\n Even when cost.amount=\"0\" (subscription), this field carries the value\n of the access for accounting purposes (e.g., ASC 606 prepaid drawdown).").optional(), "transaction_id": z.string().describe("Exchange-assigned transaction identifier.").default("") }).describe("TransactionResultItem — Result for a single offer in a batch transaction.")).describe("Per-offer results (one entry per committed item, in original order).").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("SubscriptionQuotaInfo — Proactive quota signaling for subscription access.\n\nAnalogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Post-transaction quota state. Tells the agent how much quota remains\n after this transaction. Enables proactive throttling (\"1 access left\").\n Multiple entries for multi-dimensional quotas.").optional(), "total_cost": z.object({ "amount": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Exact decimal string (not a float), e.g. \"19.99\". Denominated in `currency`.").default(""), "currency": z.string().default(""), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).optional() }).describe("Aggregate cost of the purchased items (those without denial_reason): their\n cost.amount summed as exact decimals, in their one shared currency.\n unit_cost is unset. UNSET when the purchased items span more than one\n currency: amounts are never summed across currencies, and currency\n conversion is out of scope for this version. A caller that needs the totals\n then sums each item's cost per currency itself.").optional(), "ver": z.string().describe("FORA protocol version — \"1.0\". Stamped by the sender from a single\n constant; advisory on receive. See \"Protocol version\" in the file header.").default("") }).describe("TransactionResponse — Exchange confirms the transaction(s).\n\nItems-only: every per-result datum lives in `items`\n (one TransactionResultItem per committed offer, in original order); the\n top-level fields carry only the shared aggregate state. A single offer is the\n degenerate 1-element `items`.\n\n Every per-item denial is in the body, on TransactionResultItem.denial_reason,\n whatever the item count. A one-item purchase that is denied is this response\n with its one item denied; it is never turned into a non-OK error. Only a\n refusal of the whole request, which decides no item, is a non-OK error\n carrying ErrorDetail.transaction_denial (see TransactionDenial).")))(); diff --git a/proto/CHANGELOG.md b/proto/CHANGELOG.md index cc3d44f5..92e34fbf 100644 --- a/proto/CHANGELOG.md +++ b/proto/CHANGELOG.md @@ -4,8 +4,49 @@ ### Breaking against v1.1.0 +**Protocol** + +- **Delegation profile.** The holder-bound JWT delegation chain is now a fixed profile + on the authentication page, and the Exchange verifies it as written. The chain is + one compact JWS: each child carries its complete signed parent in the `fora_parent` + claim and its own Ed25519 public key in the protected `jwk` header, at most four + tokens deep. Every token requires the protected header `typ` `fora-delegation+jwt` + and `alg` `EdDSA` or `Ed25519` with an Ed25519 key, and the claims `iss`, `aud`, + `cnf.jkt` and a finite `exp`; `nbf` is honoured, `jti` is recommended, and a claim + the profile does not list is refused, with vendor metadata inside the signed + `fora_advisory` object only. `aud` follows the Request recipient rule. The root's + `iss` is the identity configured with the trusted key, on the subscription record + that owns the gated term; a child's `iss` is the thumbprint of its own signing key. + No key is discovered through OIDC, JWKS or the token. The clock leeway is 60 seconds + and never extends the effective expiry, the chain's earliest `exp`, which bounds a + gated offer's expiry. `Delegation.issuer` and `Delegation.principal_domain` mirror + the root's `iss` and establish nothing. For a term gated behind a verified + delegation the Exchange reads scopes from the chain only and ignores + `Requester.scopes`. Revocation lookup through `Delegation.revocation_uri` is not part + of the profile: withdrawing a configured key is how a chain is invalidated early. The + tokens the documentation shows are real and verified by a conformance test. + +**Wire** + +- **Delegation token size.** `Delegation.token` is at most 32768 bytes, declared as + `bytes.max_len` on the field. The limit holds for the complete chain, every embedded + parent included, and is checked at wire validation before the token is parsed, so a + longer token is an invalid request rather than a per-item denial, whether or not the + bytes are a valid JWT. The generated Pydantic and Zod models, the published JSON + Schemas and the precompiled strict validators carry the same limit as an exact + base64 pattern, because a plain `maxLength` on the base64 string would admit a token + one byte over the limit. The validation corpus carries a 32768-byte token that is + accepted and a 32769-byte token that is refused. + **SDK users** +- **`ScopesSubset` is grant containment.** `helpers.ScopesSubset`, `scopes_subset` and + `scopesSubset` report whether every scope in the first list is covered by some scope + in the second under the protocol's segment-wise rule, not whether it is an exact + member. A child `publisher:news:read` under a parent `publisher:*:read` now passes, + as the proto always said it should, and a child `publisher:news:*` under that parent + still fails. A caller that relied on exact membership sees wildcard grants accepted. + The `subset` section of `scopes-vectors.json` carries the new cases. - **Go client errors.** `connect.CallError` and `resolvers.FetchError` gain a `Header` field, an `http.Header`, so neither struct compares with `==` any more. An RPC `CallError` now carries the `Status` of the peer's error answer, which v1.1.0 left at 0, @@ -13,6 +54,40 @@ ### Changes since v1.1.0 +**Scope matching and delegation verification in the SDKs.** + +- **Scope matching helpers.** `helpers.ScopeCovers` and `helpers.ScopesCover` (Go), + `scope_covers` and `scopes_cover` (Python), `scopeCovers` and `scopesCover` + (TypeScript) implement the protocol's segment-wise scope rule: each granted segment + equals the required one or is `*`, a terminal `*` covers the remaining segments and + needs at least one, no implicit prefix match, a grant with more segments than the + requirement never covers it, and a `*` in the requirement is a literal segment. All + three are pinned to `sdk/go/helpers/testdata/scope-match-vectors.json`, whose + verdicts are written from the rule, not emitted by an implementation. +- **Delegation chain verifier (Go).** `helpers.VerifyDelegation` verifies a chain + against the profile: the size and depth limits, the protected header, the chain walk + through `fora_parent`, the root under caller-configured `TrustedIssuer` keys, each + child under its `jwk` and the intermediate issuer rule, the claims table, the clock + leeway, attenuation of scopes, audiences and expiry, the audience and the holder + binding. It takes a `DelegationInput` and returns a `VerifiedDelegation` (root + issuer, holder thumbprint, effective scopes, effective expiry, depth) or a + `DelegationError` naming one `DelegationFailure`, which unwraps to + `ErrDelegationInvalid`. It verifies the Ed25519 signature with the standard library + under either `alg` name, because the pinned go-jose accepts only `EdDSA`. It meters + nothing, so a chain carrying `fora_max_spend_cents`, `fora_max_accesses` or + `fora_quota_period` is refused. The constants `MaxDelegationTokenBytes`, + `MaxDelegationChainDepth`, `DelegationClockLeeway`, `DelegationTokenType` and + `MaxDelegationNumericDate` are the profile's values; the last bounds `exp` and `nbf` + to `9999-12-31T23:59:59Z` so the returned expiry always fits a `Timestamp`. The + verifier accepts only unpadded base64url segments and exactly one JSON object as + payload, and refuses a `b64` header parameter. The TypeScript and Python verifiers are deferred and recorded + as a decision in the parity matrix. +- **Shared delegation chain cases.** `sdk/go/helpers/testdata/delegation-chains.json` + carries signed chains from fixed test keys and a fixed clock with outcomes declared + from the profile, one case per rule, plus the chains the documentation embeds. The + Go tests re-verify every case; an Exchange's tests and the deferred ports replay the + same file. + **Three more SDK capabilities for conformance and e2e harnesses (no wire change).** A harness can now read the header fields a refusal arrived with, sign an acceptance that names an empty requester to prove that a receiver refuses it, and, in Python, have the diff --git a/proto/fora/v1/fora.proto b/proto/fora/v1/fora.proto index ebe9556e..d489b06e 100644 --- a/proto/fora/v1/fora.proto +++ b/proto/fora/v1/fora.proto @@ -948,7 +948,9 @@ message Offer { // Post-usage reporting requirements for this offer. optional ReportingObligation reporting = 5; - // When this offer expires (ISO 8601). + // When this offer expires (ISO 8601). An offer for a term gated behind a + // verified delegation MUST NOT expire later than that delegation chain's + // effective expiry, the earliest `exp` in the chain (see Delegation). optional google.protobuf.Timestamp expires_at = 6; // Resource identity for cross-exchange deduplication. @@ -1911,16 +1913,20 @@ message LicenseTerm { // Requester.scopes is the requester's own statement, which the Exchange does // not verify, so a scope gate hides a term from requesters that do not state // the scope; it does not stop one that states it falsely. An Exchange MAY - // also require a verified delegation that grants these scopes before it - // presents the term. To a requester that presents none, it answers as if the - // term did not exist, as existence hiding requires (see Requester.scopes). An - // offer the Exchange presented is honoured until it expires. + // instead gate a term behind a verified delegation (a subscription term is + // the usual case): then only the delegation chain's verified effective scopes + // are matched against these scopes, and Requester.scopes is ignored for that + // term. To a requester that presents no valid delegation, it answers as if + // the term did not exist, as existence hiding requires (see + // Requester.scopes). An offer the Exchange presented is honoured until it + // expires, and a gated offer expires no later than the chain's effective + // expiry (see Delegation). // // Coverage uses the SAME matching rule as Requester.scopes: // segment-wise (":" separated), each granted segment must equal the // corresponding required segment or be "*", a terminal "*" matches all - // remaining segments, and there is NO implicit prefix match (a grant - // narrower than the requirement does not cover it). "dist:*" covers + // remaining segments (at least one), and there is NO implicit prefix match + // (a grant narrower than the requirement does not cover it). "dist:*" covers // "dist:US" and "dist:US:CA"; "dist" covers only "dist". There is exactly // one scope-matching algorithm across the protocol. repeated string scopes = 7 [(buf.validate.field).repeated.max_items = 64]; @@ -2258,12 +2264,18 @@ message Requester { // Matching is SEGMENT-WISE (":" separated). A granted scope G covers a // required scope R iff, segment by segment, each G segment equals the // corresponding R segment or is "*"; a terminal "*" matches all remaining - // segments. There is NO implicit prefix match, and a grant NARROWER than - // the requirement does not cover it (G must be equal-to-or-broader than R). - // Examples: "dist:*" covers "dist:US" and "dist:US:CA"; "dist:US:*" covers - // "dist:US:CA" but not "dist:EU"; bare "dist" covers only "dist"; granted - // "dist:US:CA" does NOT cover required "dist:US"; "*" covers everything. - // This same rule governs LicenseTerm.scopes — one algorithm protocol-wide. + // segments, of which there must be at least one. There is NO implicit prefix + // match, and a grant NARROWER than the requirement does not cover it (G must + // be equal-to-or-broader than R), so a grant with more segments than the + // requirement never covers it, even when its last segment is "*". A "*" in R + // is a literal segment: a narrow grant never covers a wildcard requirement. + // Matching is case-sensitive and never trims. Examples: "dist:*" covers + // "dist:US", "dist:US:CA" and "dist:US:*" but not "dist"; "dist:US:*" covers + // "dist:US:CA" but not "dist:EU" or "dist:US"; "publisher:*:read" covers + // "publisher:news:read" but not "publisher:news:*"; bare "dist" covers only + // "dist"; granted "dist:US:CA" does NOT cover required "dist:US"; "*" covers + // everything. This same rule governs LicenseTerm.scopes and the attenuation + // of a delegation child against its parent — one algorithm protocol-wide. // // When empty, Exchange applies its default access policy (typically // returns all publicly available resources). @@ -2311,62 +2323,43 @@ enum RequesterType { // Delegation — Scoped, time-limited, spend-capped credential. // -// The token is an opaque, signed credential. The format is a JWT (token_format -// "jwt"); the field stays open for a future format. Its claims are the -// AUTHORITATIVE source of the grant; the -// plaintext fields below (scopes, expires_at, max_spend_cents, …) are a -// convenience mirror the Exchange MAY use for fast pre-filtering before it -// verifies the token. -// -// Holder binding (the load-bearing property). The grant is bound to a holder key -// via the RFC 7800 `cnf` confirmation claim — `cnf.jkt`, the RFC 7638 JWK -// thumbprint of the holder's key. Verification MUST check that the key signing -// the request (RFC 9421) hashes to that thumbprint; a token possessed without -// the matching private key is rejected. This is what makes a leaked token NOT -// bearer-usable. One exception, the relayed purchase: when a Broker re-packages -// a purchase (BrokerService.ExecuteTransaction), the request an Exchange -// receives is signed by the Broker, not by the holder. There the Exchange checks -// the holder binding against the key each item's AgentAcceptance verifies under -// — the agent's key, resolved from the requester's WBA directory — and not -// against the request signer. A relayed purchase that carries a delegation -// therefore needs an AgentAcceptance on every item; an item without one fails -// the binding (DENIAL_REASON_DELEGATION_INVALID). On a query a Broker -// originated (Broker-led discovery), the Broker signs alone, so a delegation on -// it cannot pass holder binding: the Exchange does not honour it 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. An Exchange -// that requires a verified delegation before it presents a scoped term (see -// LicenseTerm.scopes) therefore presents no such term on that query. -// -// Delegation is a chain of cnf-linked JWTs: a principal narrows a -// grant by issuing a child JWT (cnf = the next holder, scopes ⊆ parent), signed -// by the key the parent's cnf named — the chain-linkage invariant. Verifiers -// need only the root issuer's public key; intermediate keys ride inside the -// chain (JOSE header `jwk`), so verification is offline. -// -// The claim schema is a small registered vocabulary — see the FORA -// delegation-claims profile in the auth spec. All claims are OPTIONAL except the -// holder binding (cnf). Vendors MAY add namespaced claims (their own "vendor:" -// namespace; "fora_"-prefixed names are reserved for the registered vocabulary -// and MUST NOT be redefined); any constraint a verifier cannot evaluate is -// binding by default (fail closed) unless the issuer marks it advisory — -// mirroring Restriction.advisory. -// -// Scope/time/spend caps are defense-in-depth that bound the blast radius only in -// the residual case where the holder's signing key is also compromised; the -// primary protection against theft is the holder binding above. +// The token is a signed JWT (token_format "jwt"; the field stays open for a +// future format) carrying a holder-bound delegation chain. Its claims are the +// AUTHORITATIVE source of the grant; the plaintext fields below (scopes, +// expires_at, max_spend_cents, …) are a mirror the Exchange MAY use for fast +// pre-filtering before it verifies the token. A mirror establishes nothing: it +// cannot grant a scope, extend validity or raise a cap beyond what the signed +// chain says. +// +// The profile — chain encoding and limits, protected header, claims, clock +// leeway, issuer, audience and holder-binding rules, the verification +// procedure and the outcome of each failure — is the "Delegation" section of +// the authentication page and is not restated here; the Go SDK verifier +// (sdk/go/helpers) and the shared chain cases are derived from it. What a +// field reader must know: the grant is bound to a holder key by `cnf.jkt`, and +// the key a request proves possession of MUST hash to the terminal token's +// `cnf.jkt` (the RFC 9421 request signer on a direct call; on a purchase a +// Broker relayed, the key each item's AgentAcceptance verifies under), so a +// token possessed without its private key grants nothing. On a query a Broker +// originated nothing can pass that binding: the Exchange does not honour the +// delegation, answers as if none were present, and presents no term gated +// behind one (see LicenseTerm.scopes). message Delegation { - // Who granted this delegation (domain for public key lookup). + // Mirror of the ROOT token's `iss`, the configured trust anchor's identity. + // Informational: no key is looked up from it, and it establishes no trust. string principal_domain = 1; // Principal's identifier (e.g., "user@acme.com", "marketdata.example.com"). + // Informational; not read by the verifier. string principal_id = 2; - // Scopes granted by this delegation. MUST be a subset of the - // principal's own scopes (attenuation — can only narrow, not widen). + // Mirror of the terminal token's effective scopes. In the chain, each + // child's scopes are covered by its parent's under the scope-matching rule + // on Requester.scopes (attenuation — can only narrow, not widen). repeated string scopes = 3; - // When this delegation expires. Exchange MUST reject expired tokens. + // Mirror of the chain's earliest `exp`. The Exchange MUST reject an expired + // token; the signed `exp` values decide, not this field. google.protobuf.Timestamp expires_at = 4; // Maximum spend in currency minor units (e.g., cents for USD). @@ -2385,21 +2378,25 @@ message Delegation { // When absent, the quota is lifetime (bounded only by expires_at). optional google.protobuf.Duration quota_period = 10; - // Token bytes. A JWT (base64url-encoded JWS). - bytes token = 6; + // Token bytes. A JWT (base64url-encoded JWS). At most 32768 bytes: the + // complete chain, every embedded parent included, must fit. The limit is a + // wire rule, checked before the token is parsed and independent of whether + // the bytes are a valid JWT; a longer token is an invalid request, not a + // per-item denial. + bytes token = 6 [(buf.validate.field).bytes.max_len = 32768]; // Token format: "jwt" (default). Empty is treated as "jwt". The field stays // open for a future format. string token_format = 7; - // Optional: URI for real-time revocation checking. - // Exchange MAY check this for high-value transactions. - // Not checked for routine low-value access (performance tradeoff). + // Informational. Revocation lookup is not part of the delegation profile: + // the verifier never fetches this URI. A chain is invalidated early by + // withdrawing the configured root key. optional string revocation_uri = 8; - // Token issuer. OIDC issuer URL or GNAP grant server URL. - // Exchange uses this for JWT validation (OIDC discovery → JWKS) - // or GNAP token introspection. + // Mirror of the ROOT token's `iss`, the identity configured with the trusted + // key (never an intermediate or terminal issuer). Informational: the + // Exchange never discovers a key from it, through OIDC, JWKS or otherwise. optional string issuer = 11; // Extension point @@ -2920,7 +2917,7 @@ enum DenialReason { // is denied and the whole request is not refused. DENIAL_REASON_SIGNATURE_INVALID = 8; DENIAL_REASON_QUOTA_EXCEEDED = 9; // Subscription access count exhausted for this period - DENIAL_REASON_DELEGATION_INVALID = 10; // Delegation missing, unverifiable, expired, holder binding failed, or scopes/caps do not cover the request + DENIAL_REASON_DELEGATION_INVALID = 10; // On a purchase item: delegation missing, malformed, unverifiable (chain linkage, issuer, signature), expired or not yet valid, addressed to another recipient, holder binding failed, a child widens its parent, or scopes/caps do not cover the request. A token over the size limit is refused at wire validation, before any item; at discovery a failed delegation hides the gated terms and reports nothing // DEPRECATED, never sent. An Exchange MUST NOT send this value. An offer the // Exchange presented is honoured until it expires, so a purchase is never // refused for scope. A resource the requester's scopes leave with no diff --git a/scripts/check-doc-conformance.sh b/scripts/check-doc-conformance.sh index c8629c70..c1bbeaf5 100755 --- a/scripts/check-doc-conformance.sh +++ b/scripts/check-doc-conformance.sh @@ -261,13 +261,10 @@ fi # remark-proto guard forbids re-introducing a hand-typed copy. The grep here would # only have searched the directive source, not the rendered values.) -# Delegation-claim registry guard. The registered JWT claims are -# named `fora_` where is a Delegation proto field. Self-extending: -# derive the registered claim names straight from the auth registry (the only -# place `fora_` underscored identifiers appear) and assert each maps to a real -# field on the Delegation proto message — so a typo'd or orphaned registry claim -# fails the build, and a newly-registered claim is checked automatically with no -# hardcoded list to drift. +# Delegation-claim registry guard. Apart from the two JWT-only claims below, +# registered claims are named `fora_` for a Delegation proto field. +# Derive claim names from the authentication page and require each remaining +# claim to map to a real field, so typos and orphaned mirror claims fail the build. # Scope the field lookup to the Delegation message body only — grepping the whole # proto would falsely accept e.g. `fora_offer_id` (offer_id exists on # TransactionItem, not Delegation). @@ -276,6 +273,10 @@ claim_n=0 while read -r claim; do [ -z "$claim" ] && continue claim_n=$((claim_n + 1)) + # Chain structure and advisory metadata live only inside the signed token. + case "$claim" in + fora_parent|fora_advisory) continue ;; + esac field=${claim#fora_} # Defensive: only [a-z_] field names are expected. Reject anything else rather # than interpolate it into the grep pattern below, so a future change to the diff --git a/scripts/gen-sdk-types.sh b/scripts/gen-sdk-types.sh index 1656e999..39f668ca 100755 --- a/scripts/gen-sdk-types.sh +++ b/scripts/gen-sdk-types.sh @@ -53,8 +53,12 @@ go run ./conformance/requiredgen "$WORK/required_fields.json" # .refine()) and gen_unique_py.py emits wire/unique.py for the wire/base.py seam, because # datamodel-codegen drops `uniqueItems` for pydantic v2. go run ./conformance/uniquegen "$WORK/unique_items.json" +# bytes_max_len.json: the protovalidate bytes.max_len view. protoschema renders it as a +# maxLength that counts base64 padding, which lets a value one byte over the limit pass; +# merge_schema and gen_jsonschema rewrite the pattern to the exact data-character bound. +go run ./conformance/byteslengen "$WORK/bytes_max_len.json" "$PY" scripts/sdk-types/merge_schema.py "$JS" gen/descriptor.binpb "$COMBINED" \ - "$WORK/required_fields.json" "$WORK/unique_items.json" + "$WORK/required_fields.json" "$WORK/unique_items.json" "$WORK/bytes_max_len.json" echo "==> 3/6 Pydantic v2 (datamodel-code-generator, --base-class + --collapse-root-models)" "$WORK/venv/bin/datamodel-codegen" \ @@ -94,7 +98,7 @@ echo "==> 5/6 published JSON Schemas (gen/jsonschema/)" # default and a strict variant. The wire decisions (field naming, Struct, int64) are # documented in the header of gen_jsonschema.py. "$PY" scripts/sdk-types/gen_jsonschema.py "$JS" gen/descriptor.binpb \ - "$WORK/required_fields.json" "$WORK/unique_items.json" gen/jsonschema + "$WORK/required_fields.json" "$WORK/unique_items.json" "$WORK/bytes_max_len.json" gen/jsonschema echo "==> 6/6 precompiled strict validators (gen/ts/strict/)" # From the strict schemas step 5 wrote, with the ajv the pinned manifest above installed. diff --git a/scripts/sdk-types/gen_jsonschema.py b/scripts/sdk-types/gen_jsonschema.py index 7af87e78..ff4a6404 100644 --- a/scripts/sdk-types/gen_jsonschema.py +++ b/scripts/sdk-types/gen_jsonschema.py @@ -71,6 +71,7 @@ FLOAT_STR_PATTERN, INT_STR_PATTERN, load_descriptor, + mark_bytes_max_len, mark_required, mark_unique, ) @@ -271,7 +272,7 @@ def walk(node, path): sys.exit(f"gen_jsonschema: {name}: a string form bypasses a numeric bound at {bad}") -def main(src_dir, desc_path, required_path, unique_path, out_dir): +def main(src_dir, desc_path, required_path, unique_path, bytes_path, out_dir): kinds = field_kinds(load_descriptor(desc_path)) raw = {} for f in sorted(glob.glob(os.path.join(src_dir, "*.schema.json"))): @@ -295,6 +296,7 @@ def main(src_dir, desc_path, required_path, unique_path, out_dir): bare = {fqn.rsplit(".", 1)[1]: messages[fqn] for fqn in fqns} mark_required(bare, json.load(open(required_path))) mark_unique(bare, json.load(open(unique_path))) + mark_bytes_max_len(bare, json.load(open(bytes_path))) os.makedirs(out_dir, exist_ok=True) for stale in glob.glob(os.path.join(out_dir, "*.json")): @@ -310,4 +312,4 @@ def main(src_dir, desc_path, required_path, unique_path, out_dir): if __name__ == "__main__": - main(*sys.argv[1:6]) + main(*sys.argv[1:7]) diff --git a/scripts/sdk-types/merge_schema.py b/scripts/sdk-types/merge_schema.py index a5158aaf..1e3314e2 100644 --- a/scripts/sdk-types/merge_schema.py +++ b/scripts/sdk-types/merge_schema.py @@ -425,7 +425,28 @@ def mark_unique(defs, unique_items): return defs -def main(src_dir, desc_path, out_file, required_path=None, unique_path=None): +def mark_bytes_max_len(defs, bytes_max_len): + """protoschema renders (buf.validate.field).bytes.max_len = n as a maxLength on the + base64 string that counts the padding characters, which is not exact: 32768 bytes and + 32769 bytes both encode to 43692 characters, so a value one byte over the limit passes + the schema while Go protovalidate rejects it. bytes_max_len (from the Go byteslengen + manifest) names the byte limit per field; rewrite the pattern to allow at most + ceil(4n/3) data characters, which is exact, and keep maxLength as the bound on the + whole string.""" + for msg, fields in bytes_max_len.items(): + d = defs.get(msg) + if not isinstance(d, dict) or "properties" not in d: + continue + for jname, n in fields.items(): + prop = d["properties"].get(jname) + if not isinstance(prop, dict): + continue + data_chars = -(-4 * n // 3) + prop["pattern"] = f"^[A-Za-z0-9+/]{{0,{data_chars}}}={{0,2}}$" + return defs + + +def main(src_dir, desc_path, out_file, required_path=None, unique_path=None, bytes_path=None): fds = load_descriptor(desc_path) enum_name = enum_names_from_descriptor(fds) field_types = field_type_names_from_descriptor(fds) @@ -493,6 +514,8 @@ def hoist_enums(o): mark_required(defs, json.load(open(required_path))) if unique_path: mark_unique(defs, json.load(open(unique_path))) + if bytes_path: + mark_bytes_max_len(defs, json.load(open(bytes_path))) defs.update(enum_defs) combined = { @@ -516,4 +539,4 @@ def hoist_enums(o): if __name__ == "__main__": - main(*sys.argv[1:6]) + main(*sys.argv[1:7]) diff --git a/sdk/go/helpers/delegation.go b/sdk/go/helpers/delegation.go new file mode 100644 index 00000000..9c5e4d02 --- /dev/null +++ b/sdk/go/helpers/delegation.go @@ -0,0 +1,543 @@ +package helpers + +import ( + "bytes" + "crypto/ed25519" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "math" + "strings" + "time" + + jose "github.com/go-jose/go-jose/v4" +) + +// Delegation chain verification — the FORA holder-bound JWT profile. +// +// The profile is the "Delegation" section of the authentication page +// (website/src/content/docs/protocol/authentication.mdx); this file is its only +// implementation and the shared chain cases in testdata/delegation-chains.json +// are derived from the page, not from this code. A Delegation.token is one +// compact JWS whose every child carries its complete signed parent in the +// fora_parent claim; the root is verified with a key the caller configured, a +// child with the Ed25519 key in its own protected jwk header, which must hash to +// the parent's cnf.jkt. Nothing is fetched: trust is the caller's input. +// +// Why the signature is checked with crypto/ed25519 and not go-jose: RFC 9864 +// registers the fully-specified alg value "Ed25519" and marks the polymorphic +// "EdDSA" deprecated, and the profile accepts both with an Ed25519 key. The +// pinned go-jose v4 knows only "EdDSA" and rejects "Ed25519" before any key is +// inspected, so a chain signed by a current issuer could never be verified +// through it. The compact serialization is three base64url segments, the +// signing input is the first two joined by ".", and an Ed25519 signature is 64 +// bytes, so verifying the original bytes with the standard library is a few +// lines and needs no header rewrite. go-jose still parses the jwk header (it +// knows the OKP form and refuses other curves) and computes the RFC 7638 +// thumbprint (Thumbprint), where it fits. + +const ( + // MaxDelegationTokenBytes is the size limit of Delegation.token: the wire + // rule bytes.max_len on the field, checked here again before any parsing and + // on every embedded parent before it is decoded. + MaxDelegationTokenBytes = 32768 + // MaxDelegationChainDepth is the most tokens a chain may hold, the root + // included. A fifth token is never decoded. + MaxDelegationChainDepth = 4 + // DelegationClockLeeway is the clock skew the exp and nbf checks allow. It + // never extends the effective expiry the verifier returns. + DelegationClockLeeway = 60 * time.Second + // DelegationTokenType is the protected header typ every token must carry. + DelegationTokenType = "fora-delegation+jwt" + // MaxDelegationNumericDate is the largest exp or nbf the profile accepts: + // 9999-12-31T23:59:59Z, the last second a protocol Timestamp can carry, so + // the effective expiry the verifier returns always fits one. The smallest + // is 0. + MaxDelegationNumericDate = 253402300799 +) + +// TrustedIssuer is one configured root key and the issuer identity it belongs +// to. A root token verifies only with a key whose Identity equals its iss claim +// exactly; several entries may share an Identity while a key is being replaced. +type TrustedIssuer struct { + Identity string + Key ed25519.PublicKey +} + +// DelegationInput is everything VerifyDelegation reads. Trust, audience, holder +// and time all come from the caller; the verifier reaches nothing else. +type DelegationInput struct { + // Token is Delegation.token: the complete chain as one compact JWS. + Token []byte + // TrustedIssuers are the configured root keys; in an Exchange they come from + // the subscription record that owns the gated term. + TrustedIssuers []TrustedIssuer + // Audience is the verifier's own identity as a bare domain, the value the + // Request recipient rule compares an aud entry with. + Audience string + // Holder is the RFC 7638 thumbprint of the key the request proved possession + // of: the RFC 9421 request key on a direct call, the AgentAcceptance key on + // a purchase a Broker relayed. + Holder string + // Now is the clock reading every token is checked against. A zero Now + // means time.Now(); an explicitly supplied Now is used as given. + Now time.Time +} + +// VerifiedDelegation is the result of a chain that passed every check. +type VerifiedDelegation struct { + // RootIssuer is the root token's iss, the identity of the trusted key that + // verified it. + RootIssuer string + // Holder is the terminal token's cnf.jkt, equal to DelegationInput.Holder. + Holder string + // Scopes are the effective scopes: the terminal token's scope claim, + // normalized. Nil when the terminal token grants no scoped access. + Scopes []string + // ExpiresAt is the earliest exp in the chain, without leeway. A gated offer + // must not expire later than this. + ExpiresAt time.Time + // Depth is the number of tokens in the chain, the root included. + Depth int +} + +// DelegationFailure names why a chain was refused. Every value maps to +// DENIAL_REASON_DELEGATION_INVALID on the wire; the name is for the journal +// and for the shared chain cases. +type DelegationFailure string + +const ( + DelegationOversized DelegationFailure = "oversized" + DelegationMalformed DelegationFailure = "malformed" + DelegationTooDeep DelegationFailure = "too_deep" + DelegationBadAlg DelegationFailure = "bad_alg" + DelegationBadTyp DelegationFailure = "bad_typ" + DelegationCritPresent DelegationFailure = "crit_present" + DelegationMissingClaim DelegationFailure = "missing_claim" + DelegationBadClaim DelegationFailure = "bad_claim" + DelegationUnknownClaim DelegationFailure = "unknown_claim" + DelegationUnsupportedRestriction DelegationFailure = "unsupported_restriction" + DelegationAdvisoryShadowsClaim DelegationFailure = "advisory_shadows_claim" + DelegationUntrustedIssuer DelegationFailure = "untrusted_issuer" + DelegationBadSignature DelegationFailure = "bad_signature" + DelegationBadJWK DelegationFailure = "bad_jwk" + DelegationJWKMismatch DelegationFailure = "jwk_mismatch" + DelegationChildIssuerMismatch DelegationFailure = "child_issuer_mismatch" + DelegationExpired DelegationFailure = "expired" + DelegationNotYetValid DelegationFailure = "not_yet_valid" + DelegationExpiryWidened DelegationFailure = "expiry_widened" + DelegationAudienceMismatch DelegationFailure = "audience_mismatch" + DelegationAudienceWidened DelegationFailure = "audience_widened" + DelegationScopeWidened DelegationFailure = "scope_widened" + DelegationHolderMismatch DelegationFailure = "holder_mismatch" +) + +// ErrDelegationInvalid is the sentinel every DelegationError unwraps to, so a +// caller can map any refusal to DENIAL_REASON_DELEGATION_INVALID with errors.Is. +var ErrDelegationInvalid = errors.New("helpers: delegation invalid") + +// DelegationError is a refusal: which check failed, on which token (1 is the +// root, Depth is the terminal token; 0 when the chain as a whole is refused) +// and a short detail for the journal. +type DelegationError struct { + Failure DelegationFailure + Token int + Detail string +} + +func (e *DelegationError) Error() string { + if e.Token == 0 { + return fmt.Sprintf("helpers: delegation invalid: %s: %s", e.Failure, e.Detail) + } + return fmt.Sprintf("helpers: delegation invalid: %s on token %d: %s", e.Failure, e.Token, e.Detail) +} + +func (e *DelegationError) Unwrap() error { return ErrDelegationInvalid } + +func refuse(f DelegationFailure, token int, detail string) error { + return &DelegationError{Failure: f, Token: token, Detail: detail} +} + +// delegationClaims lists every claim the profile allows at the top level. Any +// other claim is a restriction the verifier cannot evaluate and is refused. +var delegationClaims = map[string]bool{ + "iss": true, "aud": true, "cnf": true, "exp": true, "nbf": true, "jti": true, + "iat": true, "sub": true, "scope": true, "fora_parent": true, "fora_advisory": true, + "fora_max_spend_cents": true, "fora_max_accesses": true, "fora_quota_period": true, +} + +// delegationCaps are profile claims this verifier cannot enforce: it meters +// nothing, so a chain that carries one is refused rather than the cap dropped. +var delegationCaps = []string{"fora_max_spend_cents", "fora_max_accesses", "fora_quota_period"} + +// delegationToken is one decoded token of the chain. +type delegationToken struct { + signingInput []byte + signature []byte + alg string + jwk json.RawMessage // protected jwk header, nil when absent + claims map[string]any + iss string + aud map[string]bool // normalized recipient identities + jkt string + exp, nbf float64 + hasNbf bool + scopes []string + parent string +} + +// VerifyDelegation verifies a delegation chain against the profile and returns +// the verified grant. Every refusal is a *DelegationError that unwraps to +// ErrDelegationInvalid; the only other error is an unusable Audience, which is +// the verifier's own configuration, not the chain's fault. +func VerifyDelegation(in DelegationInput) (*VerifiedDelegation, error) { + if !IsBareDomain(in.Audience) { + return nil, fmt.Errorf("%w: %q", ErrAudienceIdentity, in.Audience) + } + if len(in.Token) > MaxDelegationTokenBytes { + return nil, refuse(DelegationOversized, 0, fmt.Sprintf("%d bytes, limit %d", len(in.Token), MaxDelegationTokenBytes)) + } + + // Walk inward: the outer token is the terminal one, the innermost the root. + // Depth and size are checked before each nested decode, so a fifth token or + // an oversized parent is never parsed. + var outerFirst []*delegationToken + raw := string(in.Token) + for { + tok, err := decodeDelegationToken(raw, len(outerFirst)) + if err != nil { + return nil, err + } + outerFirst = append(outerFirst, tok) + if tok.parent == "" { + break + } + if len(outerFirst) >= MaxDelegationChainDepth { + return nil, refuse(DelegationTooDeep, 0, fmt.Sprintf("more than %d tokens", MaxDelegationChainDepth)) + } + if len(tok.parent) > MaxDelegationTokenBytes { + return nil, refuse(DelegationOversized, 0, "embedded parent exceeds the token size limit") + } + raw = tok.parent + } + depth := len(outerFirst) + chain := make([]*delegationToken, depth) // root first + for i, tok := range outerFirst { + chain[depth-1-i] = tok + } + // Token numbers in refusals count from the root, so token i of chain is i+1. + + // Root: a configured key whose identity is the root's iss, and nothing else. + root := chain[0] + var keys []ed25519.PublicKey + for _, ti := range in.TrustedIssuers { + if ti.Identity == root.iss && len(ti.Key) == ed25519.PublicKeySize { + keys = append(keys, ti.Key) + } + } + if len(keys) == 0 { + return nil, refuse(DelegationUntrustedIssuer, 1, fmt.Sprintf("no configured key for issuer %q", root.iss)) + } + rootOK := false + for _, k := range keys { + if ed25519.Verify(k, root.signingInput, root.signature) { + rootOK = true + break + } + } + if !rootOK { + return nil, refuse(DelegationBadSignature, 1, "root signature does not verify with the configured key") + } + + // Children, outward from the root: the protected jwk is the signing key, it + // must hash to the parent's cnf.jkt, the child's iss is that thumbprint. + for i := 1; i < depth; i++ { + tok, parent := chain[i], chain[i-1] + n := i + 1 + if tok.jwk == nil { + return nil, refuse(DelegationBadJWK, n, "child carries no protected jwk header") + } + var k jose.JSONWebKey + if err := k.UnmarshalJSON(tok.jwk); err != nil { + return nil, refuse(DelegationBadJWK, n, "protected jwk does not parse: "+err.Error()) + } + pub, ok := k.Key.(ed25519.PublicKey) + if !ok || !k.IsPublic() { + return nil, refuse(DelegationBadJWK, n, "protected jwk is not an Ed25519 public key") + } + thumb, err := Thumbprint(pub) + if err != nil { + return nil, refuse(DelegationBadJWK, n, err.Error()) + } + if thumb != parent.jkt { + return nil, refuse(DelegationJWKMismatch, n, "protected jwk is not the key the parent's cnf.jkt names") + } + if tok.iss != thumb { + return nil, refuse(DelegationChildIssuerMismatch, n, "iss is not the thumbprint of the signing key") + } + if !ed25519.Verify(pub, tok.signingInput, tok.signature) { + return nil, refuse(DelegationBadSignature, n, "signature does not verify with the protected jwk") + } + } + + // Time, then attenuation against the parent, then audience, on every token. + // The clock is read once so every token is judged against the same instant. + clock := in.Now + if clock.IsZero() { + clock = time.Now() + } + now := float64(clock.Unix()) + float64(clock.Nanosecond())/1e9 + leeway := DelegationClockLeeway.Seconds() + want := normalizeDomain(in.Audience) + earliest := math.Inf(1) + for i, tok := range chain { + n := i + 1 + if now >= tok.exp+leeway { + return nil, refuse(DelegationExpired, n, "exp has passed") + } + if tok.hasNbf && now < tok.nbf-leeway { + return nil, refuse(DelegationNotYetValid, n, "nbf is in the future") + } + if tok.exp < earliest { + earliest = tok.exp + } + if i > 0 { + parent := chain[i-1] + if tok.exp > parent.exp { + return nil, refuse(DelegationExpiryWidened, n, "exp is later than the parent's") + } + for a := range tok.aud { + if !parent.aud[a] { + return nil, refuse(DelegationAudienceWidened, n, "aud names a recipient the parent did not") + } + } + if !ScopesSubset(tok.scopes, parent.scopes) { + return nil, refuse(DelegationScopeWidened, n, "scope is not covered by the parent's") + } + } + if !tok.aud[want] { + return nil, refuse(DelegationAudienceMismatch, n, "aud does not name this recipient") + } + } + + terminal := chain[depth-1] + if in.Holder == "" || terminal.jkt != in.Holder { + return nil, refuse(DelegationHolderMismatch, depth, "cnf.jkt is not the key the request proved possession of") + } + + sec, frac := math.Modf(earliest) + return &VerifiedDelegation{ + RootIssuer: root.iss, + Holder: terminal.jkt, + Scopes: NormalizeScopes(terminal.scopes), + ExpiresAt: time.Unix(int64(sec), int64(frac*1e9)).UTC(), + Depth: depth, + }, nil +} + +// decodeDelegationToken splits one compact JWS, checks its protected header +// and the shape of its claims, and leaves signature verification to the chain +// walk. index counts from the outer token; it is turned into a root-first +// token number only once the chain's depth is known, so refusals from here +// carry the position from the terminal token as a negative-free offset. +func decodeDelegationToken(raw string, index int) (*delegationToken, error) { + // Positions in refusals count from the root; at decode time the depth is + // unknown, so the position is reported later by the caller when possible. + // Here the token number is 0 (chain as a whole) with the index in the detail. + where := fmt.Sprintf("token %d from the outside", index) + parts := strings.Split(raw, ".") + if len(parts) != 3 { + return nil, refuse(DelegationMalformed, 0, where+": not a compact JWS") + } + // Go's base64 decoder skips CR and LF, so the alphabet is checked by hand: + // a segment is base64url without padding and nothing else, so the bytes + // the signature covers are exactly the bytes that were read. + for _, seg := range parts { + if seg == "" || !isBase64URL(seg) { + return nil, refuse(DelegationMalformed, 0, where+": a segment is not unpadded base64url") + } + } + hdrBytes, err := base64.RawURLEncoding.Strict().DecodeString(parts[0]) + if err != nil { + return nil, refuse(DelegationMalformed, 0, where+": protected header is not base64url") + } + payload, err := base64.RawURLEncoding.Strict().DecodeString(parts[1]) + if err != nil { + return nil, refuse(DelegationMalformed, 0, where+": payload is not base64url") + } + sig, err := base64.RawURLEncoding.Strict().DecodeString(parts[2]) + if err != nil || len(sig) != ed25519.SignatureSize { + return nil, refuse(DelegationMalformed, 0, where+": signature is not 64 base64url bytes") + } + var hdr map[string]json.RawMessage + if err := json.Unmarshal(hdrBytes, &hdr); err != nil || hdr == nil { + return nil, refuse(DelegationMalformed, 0, where+": protected header is not a JSON object") + } + tok := &delegationToken{signingInput: []byte(parts[0] + "." + parts[1]), signature: sig} + if err := json.Unmarshal(hdr["alg"], &tok.alg); err != nil || (tok.alg != "EdDSA" && tok.alg != "Ed25519") { + return nil, refuse(DelegationBadAlg, 0, where+": alg must be EdDSA or Ed25519") + } + var typ string + if err := json.Unmarshal(hdr["typ"], &typ); err != nil || typ != DelegationTokenType { + return nil, refuse(DelegationBadTyp, 0, where+": typ must be "+DelegationTokenType) + } + if _, has := hdr["crit"]; has { + return nil, refuse(DelegationCritPresent, 0, where+": crit is not allowed") + } + // RFC 7797 unencoded payloads are not part of the profile; a b64 parameter + // without crit is itself invalid under that RFC, and with crit it is caught + // above, so its presence is malformed either way. + if _, has := hdr["b64"]; has { + return nil, refuse(DelegationMalformed, 0, where+": b64 is not allowed") + } + if jwk, has := hdr["jwk"]; has { + tok.jwk = jwk + } + + // The payload is exactly one JSON object: json.Unmarshal would refuse + // trailing bytes, but the decoder is needed for UseNumber, and a decoder + // stops after the first value, so the rest of the input is checked by hand. + dec := json.NewDecoder(bytes.NewReader(payload)) + dec.UseNumber() + if err := dec.Decode(&tok.claims); err != nil || tok.claims == nil { + return nil, refuse(DelegationMalformed, 0, where+": payload is not a JSON object") + } + if _, err := dec.Token(); err != io.EOF { + return nil, refuse(DelegationMalformed, 0, where+": payload carries bytes after the JSON object") + } + if err := checkDelegationClaims(tok, where); err != nil { + return nil, err + } + return tok, nil +} + +// checkDelegationClaims applies the claims table of the profile to one token: +// required claims present with the right types, no unknown top-level claim, no +// cap this verifier cannot enforce, no profile name inside fora_advisory. +func checkDelegationClaims(tok *delegationToken, where string) error { + c := tok.claims + for name := range c { + if !delegationClaims[name] { + return refuse(DelegationUnknownClaim, 0, where+": unknown claim "+name) + } + } + for _, capName := range delegationCaps { + if _, has := c[capName]; has { + return refuse(DelegationUnsupportedRestriction, 0, where+": "+capName+" cannot be enforced by this verifier") + } + } + for _, name := range []string{"iss", "aud", "cnf", "exp"} { + if _, has := c[name]; !has { + return refuse(DelegationMissingClaim, 0, where+": "+name+" is required") + } + } + var ok bool + if tok.iss, ok = c["iss"].(string); !ok || tok.iss == "" { + return refuse(DelegationBadClaim, 0, where+": iss must be a non-empty string") + } + tok.aud = map[string]bool{} + var auds []string + switch v := c["aud"].(type) { + case string: + auds = []string{v} + case []any: + for _, e := range v { + s, ok := e.(string) + if !ok { + return refuse(DelegationBadClaim, 0, where+": aud entries must be strings") + } + auds = append(auds, s) + } + default: + return refuse(DelegationBadClaim, 0, where+": aud must be a string or an array of strings") + } + if len(auds) == 0 { + return refuse(DelegationBadClaim, 0, where+": aud must name at least one recipient") + } + for _, a := range auds { + if !IsBareDomain(a) { + return refuse(DelegationBadClaim, 0, where+": aud entry is not a bare host: "+a) + } + tok.aud[normalizeDomain(a)] = true + } + cnf, ok := c["cnf"].(map[string]any) + if !ok { + return refuse(DelegationBadClaim, 0, where+": cnf must be an object") + } + if tok.jkt, ok = cnf["jkt"].(string); !ok { + return refuse(DelegationBadClaim, 0, where+": cnf.jkt must be a string") + } + if raw, err := base64.RawURLEncoding.Strict().DecodeString(tok.jkt); err != nil || len(raw) != 32 { + return refuse(DelegationBadClaim, 0, where+": cnf.jkt is not a base64url SHA-256 thumbprint") + } + if tok.exp, ok = numericDate(c["exp"]); !ok { + return refuse(DelegationBadClaim, 0, where+": exp must be a finite number") + } + if v, has := c["nbf"]; has { + if tok.nbf, ok = numericDate(v); !ok { + return refuse(DelegationBadClaim, 0, where+": nbf must be a finite number") + } + tok.hasNbf = true + } + if v, has := c["jti"]; has { + if s, ok := v.(string); !ok || s == "" { + return refuse(DelegationBadClaim, 0, where+": jti must be a non-empty string") + } + } + if v, has := c["scope"]; has { + s, ok := v.(string) + if !ok { + return refuse(DelegationBadClaim, 0, where+": scope must be a string") + } + tok.scopes = strings.Fields(s) + } + if v, has := c["fora_parent"]; has { + s, ok := v.(string) + if !ok || s == "" { + return refuse(DelegationBadClaim, 0, where+": fora_parent must be a non-empty string") + } + tok.parent = s + } + if v, has := c["fora_advisory"]; has { + obj, ok := v.(map[string]any) + if !ok { + return refuse(DelegationBadClaim, 0, where+": fora_advisory must be an object") + } + for name := range obj { + if delegationClaims[name] { + return refuse(DelegationAdvisoryShadowsClaim, 0, where+": fora_advisory carries the profile claim "+name) + } + } + } + return nil +} + +// numericDate reads a NumericDate claim: a JSON number between 0 and +// MaxDelegationNumericDate. A value outside that range is finite but not a +// date this profile can return, so it is refused before any conversion. +func numericDate(v any) (float64, bool) { + n, ok := v.(json.Number) + if !ok { + return 0, false + } + f, err := n.Float64() + if err != nil || math.IsInf(f, 0) || math.IsNaN(f) || f < 0 || f > MaxDelegationNumericDate { + return 0, false + } + return f, true +} + +// isBase64URL reports whether s holds only the unpadded base64url alphabet. +func isBase64URL(s string) bool { + for i := 0; i < len(s); i++ { + c := s[i] + switch { + case c >= 'A' && c <= 'Z', c >= 'a' && c <= 'z', c >= '0' && c <= '9', c == '-', c == '_': + default: + return false + } + } + return true +} diff --git a/sdk/go/helpers/delegation_sign_test.go b/sdk/go/helpers/delegation_sign_test.go new file mode 100644 index 00000000..bc45f69d --- /dev/null +++ b/sdk/go/helpers/delegation_sign_test.go @@ -0,0 +1,123 @@ +package helpers + +import ( + "crypto/ed25519" + "encoding/base64" + "encoding/json" + "testing" +) + +// Test-only minting of profile tokens. The verifier never signs, so the signer +// lives beside the tests: the chain tests and the chain-case generator both +// mint with it from fixed seeds, and nothing here rewrites what it signs. + +// delegationTestKey derives a deterministic Ed25519 key pair from one seed byte. +func delegationTestKey(seed byte) (ed25519.PublicKey, ed25519.PrivateKey) { + priv := ed25519.NewKeyFromSeed(fixedSeed(seed)) + return priv.Public().(ed25519.PublicKey), priv +} + +// publicJWK renders pub as the OKP JWK the protected jwk header carries. +func publicJWK(pub ed25519.PublicKey) map[string]any { + return map[string]any{"kty": "OKP", "crv": "Ed25519", "x": base64.RawURLEncoding.EncodeToString(pub)} +} + +func mustThumbprint(t *testing.T, pub ed25519.PublicKey) string { + t.Helper() + thumb, err := Thumbprint(pub) + if err != nil { + t.Fatal(err) + } + return thumb +} + +// signCompact signs header and claims with priv as a compact JWS: the exact +// bytes the verifier checks, with no re-serialization in between. +func signCompact(t *testing.T, priv ed25519.PrivateKey, header, claims map[string]any) string { + t.Helper() + h, err := json.Marshal(header) + if err != nil { + t.Fatal(err) + } + p, err := json.Marshal(claims) + if err != nil { + t.Fatal(err) + } + input := base64.RawURLEncoding.EncodeToString(h) + "." + base64.RawURLEncoding.EncodeToString(p) + sig := ed25519.Sign(priv, []byte(input)) + return input + "." + base64.RawURLEncoding.EncodeToString(sig) +} + +// signCompactRaw signs header and payload exactly as given, bytes the JSON +// marshaller would never produce: trailing text after the object, a b64 +// parameter, a non-object payload. The signature is valid over those bytes, so +// only the syntax checks can refuse the token. +func signCompactRaw(priv ed25519.PrivateKey, header, payload string) string { + input := base64.RawURLEncoding.EncodeToString([]byte(header)) + "." + base64.RawURLEncoding.EncodeToString([]byte(payload)) + return input + "." + base64.RawURLEncoding.EncodeToString(ed25519.Sign(priv, []byte(input))) +} + +// tokenSpec describes one token of a test chain. Zero values take the profile's +// happy-path defaults so a test names only what it changes. +type tokenSpec struct { + alg string // "" → EdDSA + header map[string]any // extra or overriding protected header members; a nil value deletes + claims map[string]any // extra or overriding claims; a nil value deletes +} + +// mintChain mints a chain root-first. keys[i] signs token i; holder is the key +// token i's cnf.jkt names, which is keys[i+1] for every token but the last, where +// it is holderPub. Returns the terminal token, the one Delegation.token carries. +func mintChain(t *testing.T, keys []ed25519.PrivateKey, holderPub ed25519.PublicKey, rootIss string, aud []string, scope string, exp float64, specs []tokenSpec) string { + t.Helper() + if len(specs) != len(keys) { + t.Fatalf("mintChain: %d keys, %d specs", len(keys), len(specs)) + } + var parent string + for i, priv := range keys { + spec := specs[i] + pub := priv.Public().(ed25519.PublicKey) + var holder ed25519.PublicKey = holderPub + if i+1 < len(keys) { + holder = keys[i+1].Public().(ed25519.PublicKey) + } + alg := spec.alg + if alg == "" { + alg = "EdDSA" + } + header := map[string]any{"alg": alg, "typ": DelegationTokenType} + iss := rootIss + if i > 0 { + header["jwk"] = publicJWK(pub) + iss = mustThumbprint(t, pub) + } + claims := map[string]any{ + "iss": iss, + "aud": aud, + "exp": exp, + "cnf": map[string]any{"jkt": mustThumbprint(t, holder)}, + } + if scope != "" { + claims["scope"] = scope + } + if parent != "" { + claims["fora_parent"] = parent + } + for k, v := range spec.header { + if v == nil { + delete(header, k) + } else { + header[k] = v + } + } + for k, v := range spec.claims { + if v == nil { + delete(claims, k) + } else { + claims[k] = v + } + } + parent = signCompact(t, priv, header, claims) + } + return parent +} diff --git a/sdk/go/helpers/delegation_test.go b/sdk/go/helpers/delegation_test.go new file mode 100644 index 00000000..5b2a897b --- /dev/null +++ b/sdk/go/helpers/delegation_test.go @@ -0,0 +1,287 @@ +package helpers + +import ( + "crypto/ed25519" + "errors" + "fmt" + "strings" + "testing" + "time" +) + +// Fixed scenario shared by the chain tests: an issuer, a principal, an agent, a +// sub-agent and the agent's holder key, all from fixed seeds, verified at a +// fixed clock. exp is one hour after that clock. +var ( + dIssuerPub, dIssuerPriv = delegationTestKey(0xD1) + dPrincipalPub, dPrincipal = delegationTestKey(0xD2) + dAgentPub, dAgent = delegationTestKey(0xD3) + dSubPub, dSub = delegationTestKey(0xD4) + dHolderPub, _ = delegationTestKey(0xD5) + dOtherPub, dOther = delegationTestKey(0xD6) + dNow = time.Date(2026, 6, 1, 12, 0, 0, 0, time.UTC) + dExp = float64(dNow.Add(time.Hour).Unix()) + dAud = []string{"exchange.example"} +) + +func dInput(t *testing.T, token string) DelegationInput { + t.Helper() + return DelegationInput{ + Token: []byte(token), + TrustedIssuers: []TrustedIssuer{{Identity: "marketdata.example", Key: dIssuerPub}}, + Audience: "exchange.example", + Holder: mustThumbprint(t, dHolderPub), + Now: dNow, + } +} + +func dVerify(t *testing.T, token string) (*VerifiedDelegation, error) { + t.Helper() + return VerifyDelegation(dInput(t, token)) +} + +func wantFailure(t *testing.T, err error, f DelegationFailure) { + t.Helper() + var de *DelegationError + if !errors.As(err, &de) { + t.Fatalf("want DelegationError %s, got %v", f, err) + } + if de.Failure != f { + t.Fatalf("want failure %s, got %s (%v)", f, de.Failure, err) + } + if !errors.Is(err, ErrDelegationInvalid) { + t.Fatal("DelegationError must unwrap to ErrDelegationInvalid") + } +} + +func TestVerifyDelegation_ChainsThatPass(t *testing.T) { + keys := []ed25519.PrivateKey{dIssuerPriv, dPrincipal, dAgent, dSub} + cases := []struct { + name string + depth int + specs []tokenSpec + want []string + }{ + {"root_only_eddsa", 1, []tokenSpec{{}}, []string{"earnings:*", "quote:*"}}, + {"two_tokens_child_ed25519", 2, []tokenSpec{{}, {alg: "Ed25519", claims: map[string]any{"scope": "earnings:*"}}}, []string{"earnings:*"}}, + {"mixed_root_ed25519_child_eddsa", 2, []tokenSpec{{alg: "Ed25519"}, {claims: map[string]any{"scope": "quote:AAPL"}}}, []string{"quote:AAPL"}}, + {"four_tokens", 4, []tokenSpec{{}, {}, {alg: "Ed25519"}, {claims: map[string]any{"scope": "earnings:q1"}}}, []string{"earnings:q1"}}, + {"child_narrows_wildcard", 2, []tokenSpec{{claims: map[string]any{"scope": "publisher:*:read"}}, {claims: map[string]any{"scope": "publisher:news:read"}}}, []string{"publisher:news:read"}}, + {"child_without_scope_grants_nothing", 2, []tokenSpec{{}, {claims: map[string]any{"scope": nil}}}, nil}, + {"advisory_ignored_jti_nbf_iat_sub_accepted", 1, []tokenSpec{{claims: map[string]any{ + "jti": "md-1", "nbf": dExp - 7200, "iat": dExp - 3600, "sub": "acme", + "fora_advisory": map[string]any{"vendor": "x", "plan": map[string]any{"tier": 2}}, + }}}, []string{"earnings:*", "quote:*"}}, + {"root_jwk_ignored", 1, []tokenSpec{{header: map[string]any{"jwk": publicJWK(dOtherPub)}}}, []string{"earnings:*", "quote:*"}}, + {"kid_ignored", 1, []tokenSpec{{header: map[string]any{"kid": "md-2026"}}}, []string{"earnings:*", "quote:*"}}, + {"aud_string_and_443_folds", 1, []tokenSpec{{claims: map[string]any{"aud": "Exchange.Example:443"}}}, []string{"earnings:*", "quote:*"}}, + {"child_drops_an_audience", 2, []tokenSpec{{claims: map[string]any{"aud": []string{"exchange.example", "eu.exchange.example"}}}, {}}, []string{"earnings:*", "quote:*"}}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + tok := mintChain(t, keys[:c.depth], dHolderPub, "marketdata.example", dAud, "quote:* earnings:*", dExp, c.specs) + got, err := dVerify(t, tok) + if err != nil { + t.Fatal(err) + } + if got.RootIssuer != "marketdata.example" || got.Holder != mustThumbprint(t, dHolderPub) || got.Depth != c.depth { + t.Fatalf("unexpected result %+v", got) + } + if strings.Join(got.Scopes, " ") != strings.Join(c.want, " ") { + t.Fatalf("scopes = %v, want %v", got.Scopes, c.want) + } + if !got.ExpiresAt.Equal(dNow.Add(time.Hour)) { + t.Fatalf("expiry = %v", got.ExpiresAt) + } + }) + } +} + +func TestVerifyDelegation_EffectiveExpiryIsTheEarliest(t *testing.T) { + keys := []ed25519.PrivateKey{dIssuerPriv, dPrincipal, dAgent} + tok := mintChain(t, keys, dHolderPub, "marketdata.example", dAud, "quote:*", dExp, []tokenSpec{ + {}, {claims: map[string]any{"exp": dExp - 600}}, {claims: map[string]any{"exp": dExp - 600}}, + }) + got, err := dVerify(t, tok) + if err != nil { + t.Fatal(err) + } + if !got.ExpiresAt.Equal(dNow.Add(50 * time.Minute)) { + t.Fatalf("expiry = %v, want the children's", got.ExpiresAt) + } +} + +func TestVerifyDelegation_Refusals(t *testing.T) { + keys := []ed25519.PrivateKey{dIssuerPriv, dPrincipal, dAgent, dSub} + root1 := func(spec tokenSpec) string { + return mintChain(t, keys[:1], dHolderPub, "marketdata.example", dAud, "quote:*", dExp, []tokenSpec{spec}) + } + chain2 := func(rootSpec, childSpec tokenSpec) string { + return mintChain(t, keys[:2], dHolderPub, "marketdata.example", dAud, "quote:* publisher:*:read", dExp, []tokenSpec{rootSpec, childSpec}) + } + ecJWK := map[string]any{"kty": "EC", "crv": "P-256", "x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU", "y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0"} + ed448JWK := map[string]any{"kty": "OKP", "crv": "Ed448", "x": strings.Repeat("A", 76)} + cases := []struct { + name string + token string + want DelegationFailure + }{ + {"not_a_jws", "abc", DelegationMalformed}, + {"alg_es256", root1(tokenSpec{alg: "ES256"}), DelegationBadAlg}, + {"typ_missing", root1(tokenSpec{header: map[string]any{"typ": nil}}), DelegationBadTyp}, + {"typ_jwt", root1(tokenSpec{header: map[string]any{"typ": "JWT"}}), DelegationBadTyp}, + {"crit_present", root1(tokenSpec{header: map[string]any{"crit": []string{"b64"}, "b64": false}}), DelegationCritPresent}, + {"iss_missing", root1(tokenSpec{claims: map[string]any{"iss": nil}}), DelegationMissingClaim}, + {"aud_missing", root1(tokenSpec{claims: map[string]any{"aud": nil}}), DelegationMissingClaim}, + {"cnf_missing", root1(tokenSpec{claims: map[string]any{"cnf": nil}}), DelegationMissingClaim}, + {"exp_missing", root1(tokenSpec{claims: map[string]any{"exp": nil}}), DelegationMissingClaim}, + {"exp_string", root1(tokenSpec{claims: map[string]any{"exp": "1781787345"}}), DelegationBadClaim}, + {"nbf_string", root1(tokenSpec{claims: map[string]any{"nbf": "soon"}}), DelegationBadClaim}, + {"jti_empty", root1(tokenSpec{claims: map[string]any{"jti": ""}}), DelegationBadClaim}, + {"aud_not_bare_host", root1(tokenSpec{claims: map[string]any{"aud": "https://exchange.example"}}), DelegationBadClaim}, + {"aud_empty_array", root1(tokenSpec{claims: map[string]any{"aud": []string{}}}), DelegationBadClaim}, + {"cnf_jkt_not_thumbprint", root1(tokenSpec{claims: map[string]any{"cnf": map[string]any{"jkt": "short"}}}), DelegationBadClaim}, + {"unknown_claim", root1(tokenSpec{claims: map[string]any{"vendor:plan": "gold"}}), DelegationUnknownClaim}, + {"spend_cap_unsupported", root1(tokenSpec{claims: map[string]any{"fora_max_spend_cents": 50000}}), DelegationUnsupportedRestriction}, + {"access_cap_unsupported", root1(tokenSpec{claims: map[string]any{"fora_max_accesses": 10}}), DelegationUnsupportedRestriction}, + {"advisory_shadows_scope", root1(tokenSpec{claims: map[string]any{"fora_advisory": map[string]any{"scope": "*"}}}), DelegationAdvisoryShadowsClaim}, + {"advisory_not_object", root1(tokenSpec{claims: map[string]any{"fora_advisory": "x"}}), DelegationBadClaim}, + {"untrusted_issuer", root1(tokenSpec{claims: map[string]any{"iss": "other.example"}}), DelegationUntrustedIssuer}, + {"root_signed_by_other_key", mintChain(t, []ed25519.PrivateKey{dOther}, dHolderPub, "marketdata.example", dAud, "quote:*", dExp, []tokenSpec{{}}), DelegationBadSignature}, + {"expired_at_leeway_boundary", root1(tokenSpec{claims: map[string]any{"exp": float64(dNow.Unix()) - 60}}), DelegationExpired}, + {"not_yet_valid_beyond_leeway", root1(tokenSpec{claims: map[string]any{"nbf": float64(dNow.Unix()) + 61}}), DelegationNotYetValid}, + {"audience_other_recipient", root1(tokenSpec{claims: map[string]any{"aud": "eu.exchange.example"}}), DelegationAudienceMismatch}, + {"audience_other_port", root1(tokenSpec{claims: map[string]any{"aud": "exchange.example:8443"}}), DelegationAudienceMismatch}, + {"holder_mismatch", mintChain(t, keys[:1], dOtherPub, "marketdata.example", dAud, "quote:*", dExp, []tokenSpec{{}}), DelegationHolderMismatch}, + {"child_without_jwk", chain2(tokenSpec{}, tokenSpec{header: map[string]any{"jwk": nil}}), DelegationBadJWK}, + {"child_jwk_ec", chain2(tokenSpec{}, tokenSpec{header: map[string]any{"jwk": ecJWK}}), DelegationBadJWK}, + {"child_jwk_ed448", chain2(tokenSpec{}, tokenSpec{header: map[string]any{"jwk": ed448JWK}}), DelegationBadJWK}, + {"child_jwk_not_parents_cnf", chain2(tokenSpec{}, tokenSpec{header: map[string]any{"jwk": publicJWK(dOtherPub)}}), DelegationJWKMismatch}, + {"child_iss_wrong_with_valid_linkage", chain2(tokenSpec{}, tokenSpec{claims: map[string]any{"iss": "acme"}}), DelegationChildIssuerMismatch}, + {"child_signed_by_other_key", forgedChild(t), DelegationBadSignature}, + {"child_exp_later_than_parent", chain2(tokenSpec{}, tokenSpec{claims: map[string]any{"exp": dExp + 1}}), DelegationExpiryWidened}, + {"child_adds_audience", chain2(tokenSpec{}, tokenSpec{claims: map[string]any{"aud": []string{"exchange.example", "eu.exchange.example"}}}), DelegationAudienceWidened}, + {"child_scope_outside_parent", chain2(tokenSpec{}, tokenSpec{claims: map[string]any{"scope": "credit:read"}}), DelegationScopeWidened}, + {"child_wildcard_widens_parent", chain2(tokenSpec{}, tokenSpec{claims: map[string]any{"scope": "publisher:news:*"}}), DelegationScopeWidened}, + {"child_scope_under_scopeless_parent", chain2(tokenSpec{claims: map[string]any{"scope": nil}}, tokenSpec{claims: map[string]any{"scope": "quote:*"}}), DelegationScopeWidened}, + {"five_tokens", mintChain(t, []ed25519.PrivateKey{dIssuerPriv, dPrincipal, dAgent, dSub, dOther}, dHolderPub, "marketdata.example", dAud, "quote:*", dExp, []tokenSpec{{}, {}, {}, {}, {}}), DelegationTooDeep}, + {"fora_parent_not_a_jws", root1(tokenSpec{claims: map[string]any{"fora_parent": "nope"}}), DelegationMalformed}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + _, err := dVerify(t, c.token) + wantFailure(t, err, c.want) + }) + } +} + +func TestVerifyDelegation_RawSyntax(t *testing.T) { + // A valid root, as raw header and payload strings, then mutated below the + // JSON layer: every mutation keeps a valid signature over the bytes sent. + header := `{"alg":"EdDSA","typ":"fora-delegation+jwt"}` + payload := `{"iss":"marketdata.example","aud":["exchange.example"],"exp":` + strconvF(dExp) + `,"scope":"quote:*","cnf":{"jkt":"` + mustThumbprint(t, dHolderPub) + `"}}` + good := signCompactRaw(dIssuerPriv, header, payload) + if _, err := dVerify(t, good); err != nil { + t.Fatalf("baseline raw token must verify: %v", err) + } + cases := []struct { + name string + token string + want DelegationFailure + }{ + {"trailing_second_object", signCompactRaw(dIssuerPriv, header, payload+` {"scope":"*"}`), DelegationMalformed}, + {"trailing_garbage", signCompactRaw(dIssuerPriv, header, payload+`junk`), DelegationMalformed}, + {"payload_not_an_object", signCompactRaw(dIssuerPriv, header, `[1]`), DelegationMalformed}, + {"header_trailing_garbage", signCompactRaw(dIssuerPriv, header+`x`, payload), DelegationMalformed}, + {"b64_false_without_crit", signCompactRaw(dIssuerPriv, `{"alg":"EdDSA","typ":"fora-delegation+jwt","b64":false}`, payload), DelegationMalformed}, + {"crlf_after_signature", good + "\r\n", DelegationMalformed}, + {"crlf_inside_signature", good[:len(good)-4] + "\r\n" + good[len(good)-4:], DelegationMalformed}, + {"padded_segment", good + "=", DelegationMalformed}, + {"empty_segment", strings.Replace(good, ".", "..", 1), DelegationMalformed}, + {"exp_1e100", signCompactRaw(dIssuerPriv, header, strings.Replace(payload, strconvF(dExp), "1e100", 1)), DelegationBadClaim}, + {"exp_negative", signCompactRaw(dIssuerPriv, header, strings.Replace(payload, strconvF(dExp), "-1", 1)), DelegationBadClaim}, + {"nbf_beyond_range", signCompactRaw(dIssuerPriv, header, strings.Replace(payload, `"scope"`, `"nbf":253402300800,"scope"`, 1)), DelegationBadClaim}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + _, err := dVerify(t, c.token) + wantFailure(t, err, c.want) + }) + } + max := signCompactRaw(dIssuerPriv, header, strings.Replace(payload, strconvF(dExp), "253402300799", 1)) + got, err := dVerify(t, max) + if err != nil || got.ExpiresAt.Year() != 9999 { + t.Fatalf("the largest NumericDate must verify and convert exactly: %v %v", got, err) + } +} + +func strconvF(f float64) string { return fmt.Sprintf("%d", int64(f)) } + +func TestVerifyDelegation_SizeLimitBeforeParsing(t *testing.T) { + in := dInput(t, strings.Repeat("a", MaxDelegationTokenBytes+1)) + _, err := VerifyDelegation(in) + wantFailure(t, err, DelegationOversized) + in.Token = []byte(strings.Repeat("a", MaxDelegationTokenBytes)) + _, err = VerifyDelegation(in) + wantFailure(t, err, DelegationMalformed) // size passed, structure did not +} + +func TestVerifyDelegation_ParentSubstitution(t *testing.T) { + // A child minted under one root is re-attached to another valid root whose + // cnf names a different principal: the child's jwk no longer matches. + keys := []ed25519.PrivateKey{dIssuerPriv, dPrincipal} + good := mintChain(t, keys, dHolderPub, "marketdata.example", dAud, "quote:*", dExp, []tokenSpec{{}, {}}) + otherRoot := mintChain(t, []ed25519.PrivateKey{dIssuerPriv}, dOtherPub, "marketdata.example", dAud, "quote:*", dExp, []tokenSpec{{}}) + child := mintChain(t, keys, dHolderPub, "marketdata.example", dAud, "quote:*", dExp, []tokenSpec{{}, {claims: map[string]any{"fora_parent": otherRoot}}}) + if _, err := dVerify(t, good); err != nil { + t.Fatal(err) + } + _, err := dVerify(t, child) + wantFailure(t, err, DelegationJWKMismatch) +} + +func TestVerifyDelegation_LeewayBoundaries(t *testing.T) { + root := func(claims map[string]any) string { + return mintChain(t, []ed25519.PrivateKey{dIssuerPriv}, dHolderPub, "marketdata.example", dAud, "quote:*", dExp, []tokenSpec{{claims: claims}}) + } + now := float64(dNow.Unix()) + if _, err := dVerify(t, root(map[string]any{"exp": now - 59})); err != nil { + t.Fatalf("59s past exp is inside the leeway: %v", err) + } + got, err := dVerify(t, root(map[string]any{"exp": now - 59})) + if err != nil || !got.ExpiresAt.Before(dNow) { + t.Fatalf("leeway must not extend the returned expiry: %v %v", got, err) + } + if _, err := dVerify(t, root(map[string]any{"nbf": now + 60})); err != nil { + t.Fatalf("nbf 60s ahead is inside the leeway: %v", err) + } +} + +func TestVerifyDelegation_ZeroNowIsTheCurrentTime(t *testing.T) { + expired := float64(time.Now().Add(-365 * 24 * time.Hour).Unix()) + in := dInput(t, mintChain(t, []ed25519.PrivateKey{dIssuerPriv}, dHolderPub, "marketdata.example", dAud, "quote:*", expired, []tokenSpec{{}})) + in.Now = time.Time{} + _, err := VerifyDelegation(in) + wantFailure(t, err, DelegationExpired) +} + +func TestVerifyDelegation_BadAudienceConfiguration(t *testing.T) { + in := dInput(t, "a.b.c") + in.Audience = "https://exchange.example" + _, err := VerifyDelegation(in) + if !errors.Is(err, ErrAudienceIdentity) || errors.Is(err, ErrDelegationInvalid) { + t.Fatalf("a bad verifier identity is a configuration error, not a refusal: %v", err) + } +} + +// forgedChild is a child whose jwk and iss name the principal the root's cnf +// names, but whose signature was made with another key: linkage looks right +// and only the signature check can refuse it. +func forgedChild(t *testing.T) string { + t.Helper() + root := mintChain(t, []ed25519.PrivateKey{dIssuerPriv}, dPrincipalPub, "marketdata.example", dAud, "quote:*", dExp, []tokenSpec{{}}) + return signCompact(t, dOther, + map[string]any{"alg": "EdDSA", "typ": DelegationTokenType, "jwk": publicJWK(dPrincipalPub)}, + map[string]any{"iss": mustThumbprint(t, dPrincipalPub), "aud": dAud, "exp": dExp, + "cnf": map[string]any{"jkt": mustThumbprint(t, dHolderPub)}, "scope": "quote:*", "fora_parent": root}) +} diff --git a/sdk/go/helpers/gen_delegation_chains_test.go b/sdk/go/helpers/gen_delegation_chains_test.go new file mode 100644 index 00000000..59cece82 --- /dev/null +++ b/sdk/go/helpers/gen_delegation_chains_test.go @@ -0,0 +1,384 @@ +package helpers + +import ( + "crypto/ed25519" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + "time" +) + +// Delegation chain cases — shared, signed, with declared outcomes. +// +// TestGenerateDelegationChains mints every case from fixed seeds and a fixed +// clock and writes testdata/delegation-chains.json under FORA_UPDATE_VECTORS=1; +// otherwise it asserts the committed file matches a fresh emit. Every expected +// outcome is DECLARED in the case table from the profile text, never read back +// from VerifyDelegation; TestDelegationChainsReplay then drives the committed +// file through the verifier and fails on any disagreement. An Exchange's own +// tests, and the TypeScript and Python verifiers when they land, replay the same +// file. It is deliberately not named *-vectors.json: that suffix is the promise +// of tri-language replay, which the deferred ports cannot keep yet (see the +// delegation verifier DECISION in docs/sdk-parity-matrix.md). +// +// Every refusal declares token_index under the DelegationError.Token contract: +// 0 when the refusal is found while a token is decoded, before the chain depth +// is known (size, syntax, header and claim checks), otherwise the root-relative +// position, 1 for the root and depth for the terminal token. +// +// A case's mirrors are the plaintext Delegation fields an Exchange receives +// next to the token. Nothing in this repository reads them: the verifier takes +// only the token, and the handler example in conformance/docgo is compiled, +// not run. Only an Exchange's own tests replay the mirrors, by sending the +// case's Delegation message and checking the offers come out as declared. + +// chainFileNote is written at the top of delegation-chains.json so a reader of +// the file sees the two contracts above without opening this generator. +const chainFileNote = "Signed delegation chain cases with declared outcomes; regenerate with FORA_UPDATE_VECTORS=1 in sdk/go/helpers. " + + "expect.token_index is 0 when the refusal is found while decoding, before the chain depth is known (size, syntax, header and claim checks), " + + "otherwise the root-relative position, 1 for the root. mirrors are the plaintext Delegation fields next to the token; " + + "only an Exchange's own tests replay them, nothing in the protocol repository reads them." + +type chainFile struct { + Note string `json:"note"` + Cases []chainCase `json:"cases"` +} + +type chainTrust struct { + Identity string `json:"identity"` + KeyX string `json:"key_x"` // base64url Ed25519 public key, the OKP "x" +} + +type chainMirrors struct { + Issuer string `json:"issuer"` + PrincipalDomain string `json:"principal_domain"` + Scopes []string `json:"scopes"` + ExpiresAt string `json:"expires_at"` +} + +type chainExpect struct { + Valid bool `json:"valid"` + Failure string `json:"failure,omitempty"` + Token int `json:"token_index,omitempty"` // DelegationError.Token for a refusal + RootIssuer string `json:"root_issuer,omitempty"` + Holder string `json:"holder,omitempty"` + Scopes []string `json:"scopes,omitempty"` + ExpiresAt string `json:"expires_at,omitempty"` + Depth int `json:"depth,omitempty"` +} + +type chainCase struct { + Name string `json:"name"` + Note string `json:"note"` + Token string `json:"token"` + Trusted []chainTrust `json:"trusted"` + Audience string `json:"audience"` + Holder string `json:"holder"` + Now string `json:"now"` + Mirrors *chainMirrors `json:"mirrors,omitempty"` + Expect chainExpect `json:"expect"` +} + +func buildDelegationChains(t *testing.T) []chainCase { + t.Helper() + issuerPub, issuer := delegationTestKey(0xA1) + principalPub, principal := delegationTestKey(0xA2) + agentPub, agent := delegationTestKey(0xA3) + subPub, sub := delegationTestKey(0xA4) + holderPub, _ := delegationTestKey(0xA5) + otherPub, other := delegationTestKey(0xA6) + issuer2Pub, issuer2 := delegationTestKey(0xA7) // the replacement key for iss + _ = subPub + now := time.Date(2026, 6, 1, 12, 0, 0, 0, time.UTC) + exp := float64(now.Add(time.Hour).Unix()) + expStr := now.Add(time.Hour).UTC().Format(time.RFC3339) + nowStr := now.Format(time.RFC3339) + nowF := float64(now.Unix()) + aud := []string{"exchange.example"} + const iss = "marketdata.example" + trusted := []chainTrust{{Identity: iss, KeyX: base64.RawURLEncoding.EncodeToString(issuerPub)}} + trustedBoth := append(trusted, chainTrust{Identity: iss, KeyX: base64.RawURLEncoding.EncodeToString(issuer2Pub)}) + trustedNew := trustedBoth[1:] + trustedBy := func(tr []chainTrust) func(*chainCase) { return func(e *chainCase) { e.Trusted = tr } } + holder := mustThumbprint(t, holderPub) + thumb := func(pub ed25519.PublicKey) string { return mustThumbprint(t, pub) } + keys := []ed25519.PrivateKey{issuer, principal, agent, sub} + + mint := func(depth int, scope string, specs ...tokenSpec) string { + for len(specs) < depth { + specs = append(specs, tokenSpec{}) + } + return mintChain(t, keys[:depth], holderPub, iss, aud, scope, exp, specs) + } + ok := func(scopes []string, depth int) chainExpect { + return chainExpect{Valid: true, RootIssuer: iss, Holder: holder, Scopes: scopes, ExpiresAt: expStr, Depth: depth} + } + okAt := func(scopes []string, depth int, expiresAt time.Time) chainExpect { + e := ok(scopes, depth) + e.ExpiresAt = expiresAt.UTC().Format(time.RFC3339) + return e + } + fail := func(f DelegationFailure, token int) chainExpect { + return chainExpect{Valid: false, Failure: string(f), Token: token} + } + c := func(name, note, token string, expect chainExpect) chainCase { + return chainCase{Name: name, Note: note, Token: token, Trusted: trusted, Audience: "exchange.example", Holder: holder, Now: nowStr, Expect: expect} + } + with := func(cc chainCase, f func(*chainCase)) chainCase { f(&cc); return cc } + + // Parent substitution: a child minted under a root that names the principal, + // re-attached to a root that names another key. + otherRoot := mintChain(t, keys[:1], otherPub, iss, aud, "quote:*", exp, []tokenSpec{{}}) + // A child whose linkage is right but whose signature was made with another key. + rootForPrincipal := mintChain(t, keys[:1], principalPub, iss, aud, "quote:*", exp, []tokenSpec{{}}) + forged := signCompact(t, other, + map[string]any{"alg": "EdDSA", "typ": DelegationTokenType, "jwk": publicJWK(principalPub)}, + map[string]any{"iss": thumb(principalPub), "aud": aud, "exp": exp, "cnf": map[string]any{"jkt": holder}, "scope": "quote:*", "fora_parent": rootForPrincipal}) + ecJWK := map[string]any{"kty": "EC", "crv": "P-256", "x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU", "y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0"} + + cases := []chainCase{ + c("root_only_eddsa", "one token, alg EdDSA, holder bound to the request key", mint(1, "quote:* earnings:*"), ok([]string{"earnings:*", "quote:*"}, 1)), + c("root_only_ed25519", "one token, alg Ed25519 (RFC 9864)", mint(1, "quote:*", tokenSpec{alg: "Ed25519"}), ok([]string{"quote:*"}, 1)), + c("two_tokens", "root EdDSA, child Ed25519 narrowing scope", mint(2, "quote:* earnings:*", tokenSpec{}, tokenSpec{alg: "Ed25519", claims: map[string]any{"scope": "earnings:*"}}), ok([]string{"earnings:*"}, 2)), + c("two_tokens_mixed_reverse", "root Ed25519, child EdDSA", mint(2, "quote:*", tokenSpec{alg: "Ed25519"}, tokenSpec{}), ok([]string{"quote:*"}, 2)), + c("three_tokens", "root, principal, agent", mint(3, "quote:*", tokenSpec{}, tokenSpec{}, tokenSpec{claims: map[string]any{"scope": "quote:AAPL"}}), ok([]string{"quote:AAPL"}, 3)), + c("four_tokens", "the deepest chain the profile allows", mint(4, "quote:*", tokenSpec{}, tokenSpec{}, tokenSpec{}, tokenSpec{claims: map[string]any{"scope": "quote:AAPL"}}), ok([]string{"quote:AAPL"}, 4)), + c("five_tokens", "a fifth token is never decoded", mintChain(t, []ed25519.PrivateKey{issuer, principal, agent, sub, other}, holderPub, iss, aud, "quote:*", exp, []tokenSpec{{}, {}, {}, {}, {}}), fail(DelegationTooDeep, 0)), + c("five_tokens_fifth_malformed", "the fifth token is not a compact JWS; depth is counted before it would be decoded, so the outcome is too_deep, not malformed", mint(4, "quote:*", tokenSpec{claims: map[string]any{"fora_parent": "not-a-compact-jws"}}), fail(DelegationTooDeep, 0)), + c("jti_present", "jti is recommended and checked for shape only", mint(1, "quote:*", tokenSpec{claims: map[string]any{"jti": "md-2026-000481"}}), ok([]string{"quote:*"}, 1)), + c("jti_empty", "an empty jti is a bad claim", mint(1, "quote:*", tokenSpec{claims: map[string]any{"jti": ""}}), fail(DelegationBadClaim, 0)), + c("typ_missing", "typ fora-delegation+jwt is required on every token", mint(1, "quote:*", tokenSpec{header: map[string]any{"typ": nil}}), fail(DelegationBadTyp, 0)), + c("typ_wrong_on_child", "typ JWT on a child", mint(2, "quote:*", tokenSpec{}, tokenSpec{header: map[string]any{"typ": "JWT"}}), fail(DelegationBadTyp, 0)), + c("root_typ_wrong_under_child", "typ JWT on the root under a valid child: every token's header is checked", mint(2, "quote:*", tokenSpec{header: map[string]any{"typ": "JWT"}}, tokenSpec{}), fail(DelegationBadTyp, 0)), + c("middle_typ_wrong", "typ JWT on the middle token of three", mint(3, "quote:*", tokenSpec{}, tokenSpec{header: map[string]any{"typ": "JWT"}}, tokenSpec{}), fail(DelegationBadTyp, 0)), + c("alg_es256", "only EdDSA and Ed25519 are accepted", mint(1, "quote:*", tokenSpec{alg: "ES256"}), fail(DelegationBadAlg, 0)), + c("crit_present", "the profile defines no critical header parameters", mint(1, "quote:*", tokenSpec{header: map[string]any{"crit": []string{"b64"}, "b64": false}}), fail(DelegationCritPresent, 0)), + c("iss_missing", "", mint(1, "quote:*", tokenSpec{claims: map[string]any{"iss": nil}}), fail(DelegationMissingClaim, 0)), + c("aud_missing", "", mint(1, "quote:*", tokenSpec{claims: map[string]any{"aud": nil}}), fail(DelegationMissingClaim, 0)), + c("cnf_missing", "", mint(1, "quote:*", tokenSpec{claims: map[string]any{"cnf": nil}}), fail(DelegationMissingClaim, 0)), + c("exp_missing", "", mint(1, "quote:*", tokenSpec{claims: map[string]any{"exp": nil}}), fail(DelegationMissingClaim, 0)), + c("exp_not_a_number", "exp as a string", mint(1, "quote:*", tokenSpec{claims: map[string]any{"exp": "1781787345"}}), fail(DelegationBadClaim, 0)), + c("exp_out_of_range", "exp 1e400 is not a finite number", mint(1, "quote:*", tokenSpec{claims: map[string]any{"exp": json.Number("1e400")}}), fail(DelegationBadClaim, 0)), + c("exp_inside_leeway", "59 seconds past exp is accepted; the returned expiry is still exp", mint(1, "quote:*", tokenSpec{claims: map[string]any{"exp": nowF - 59}}), okAt([]string{"quote:*"}, 1, now.Add(-59*time.Second))), + c("exp_at_leeway", "60 seconds past exp is expired", mint(1, "quote:*", tokenSpec{claims: map[string]any{"exp": nowF - 60}}), fail(DelegationExpired, 1)), + c("nbf_inside_leeway", "nbf 60 seconds ahead is accepted", mint(1, "quote:*", tokenSpec{claims: map[string]any{"nbf": nowF + 60}}), ok([]string{"quote:*"}, 1)), + c("nbf_beyond_leeway", "nbf 61 seconds ahead is not yet valid", mint(1, "quote:*", tokenSpec{claims: map[string]any{"nbf": nowF + 61}}), fail(DelegationNotYetValid, 1)), + c("root_nbf_beyond_leeway", "nbf on the root only, under a valid child: time is checked on every token", mint(2, "quote:*", tokenSpec{claims: map[string]any{"nbf": nowF + 61}}, tokenSpec{}), fail(DelegationNotYetValid, 1)), + c("middle_nbf_beyond_leeway", "nbf on the middle token of three", mint(3, "quote:*", tokenSpec{}, tokenSpec{claims: map[string]any{"nbf": nowF + 61}}, tokenSpec{}), fail(DelegationNotYetValid, 2)), + c("child_expires_first", "the effective expiry is the earliest exp in the chain", mint(2, "quote:*", tokenSpec{}, tokenSpec{claims: map[string]any{"exp": exp - 600}}), okAt([]string{"quote:*"}, 2, now.Add(50*time.Minute))), + c("audience_other_recipient", "aud names another Exchange", mint(1, "quote:*", tokenSpec{claims: map[string]any{"aud": "eu.exchange.example"}}), fail(DelegationAudienceMismatch, 1)), + c("audience_case_and_443_fold", "Exchange.Example:443 names exchange.example", mint(1, "quote:*", tokenSpec{claims: map[string]any{"aud": "Exchange.Example:443"}}), ok([]string{"quote:*"}, 1)), + c("audience_not_bare_host", "a URL is not a recipient identity", mint(1, "quote:*", tokenSpec{claims: map[string]any{"aud": "https://exchange.example"}}), fail(DelegationBadClaim, 0)), + c("child_widens_audience", "a child adds a recipient its parent did not list", mint(2, "quote:*", tokenSpec{}, tokenSpec{claims: map[string]any{"aud": []string{"exchange.example", "eu.exchange.example"}}}), fail(DelegationAudienceWidened, 2)), + c("child_narrows_audience", "a child drops one of the parent's recipients", mint(2, "quote:*", tokenSpec{claims: map[string]any{"aud": []string{"exchange.example", "eu.exchange.example"}}}, tokenSpec{}), ok([]string{"quote:*"}, 2)), + c("untrusted_root", "the root is signed by a key that is not configured", mintChain(t, []ed25519.PrivateKey{other}, holderPub, iss, aud, "quote:*", exp, []tokenSpec{{}}), fail(DelegationBadSignature, 1)), + with(c("issuer_two_keys_old_signed", "two keys listed under one identity while a key is being replaced; the root signed with the old key verifies", mint(1, "quote:*"), ok([]string{"quote:*"}, 1)), trustedBy(trustedBoth)), + with(c("issuer_two_keys_new_signed", "two keys listed under one identity; the root signed with the new key verifies", mintChain(t, []ed25519.PrivateKey{issuer2}, holderPub, iss, aud, "quote:*", exp, []tokenSpec{{}}), ok([]string{"quote:*"}, 1)), trustedBy(trustedBoth)), + with(c("issuer_key_withdrawn", "only the new key is listed; a root signed with the withdrawn key is refused", mint(1, "quote:*"), fail(DelegationBadSignature, 1)), trustedBy(trustedNew)), + c("root_iss_not_configured", "root iss names an identity with no configured key", mint(1, "quote:*", tokenSpec{claims: map[string]any{"iss": "other.example"}}), fail(DelegationUntrustedIssuer, 1)), + c("root_signature_tampered", "the root payload was changed after signing", tamperPayload(t, mint(1, "quote:*")), fail(DelegationBadSignature, 1)), + c("child_signature_tampered", "the child payload was changed after signing", tamperPayload(t, mint(2, "quote:*")), fail(DelegationBadSignature, 2)), + c("child_iss_wrong", "signature and jwk linkage are valid but iss is not the signing key's thumbprint", mint(2, "quote:*", tokenSpec{}, tokenSpec{claims: map[string]any{"iss": "acme"}}), fail(DelegationChildIssuerMismatch, 2)), + c("child_signed_by_other_key", "jwk and iss name the principal, the signature was made with another key", forged, fail(DelegationBadSignature, 2)), + c("parent_substituted", "a child re-attached to a different valid root", mint(2, "quote:*", tokenSpec{}, tokenSpec{claims: map[string]any{"fora_parent": otherRoot}}), fail(DelegationJWKMismatch, 2)), + c("child_jwk_not_parents_cnf", "the child's jwk is not the key the parent's cnf.jkt names", mint(2, "quote:*", tokenSpec{}, tokenSpec{header: map[string]any{"jwk": publicJWK(otherPub)}}), fail(DelegationJWKMismatch, 2)), + c("child_jwk_missing", "", mint(2, "quote:*", tokenSpec{}, tokenSpec{header: map[string]any{"jwk": nil}}), fail(DelegationBadJWK, 2)), + c("child_jwk_not_ed25519", "an EC P-256 jwk", mint(2, "quote:*", tokenSpec{}, tokenSpec{header: map[string]any{"jwk": ecJWK}}), fail(DelegationBadJWK, 2)), + c("root_jwk_ignored", "a jwk on the root is not read; the configured key verifies it", mint(1, "quote:*", tokenSpec{header: map[string]any{"jwk": publicJWK(otherPub)}}), ok([]string{"quote:*"}, 1)), + c("holder_mismatch", "the terminal cnf.jkt is not the key the request proved", mintChain(t, keys[:1], agentPub, iss, aud, "quote:*", exp, []tokenSpec{{}}), fail(DelegationHolderMismatch, 1)), + c("oversized_token", "a token over 32768 bytes is refused before parsing", mint(1, "quote:*", tokenSpec{claims: map[string]any{"fora_advisory": map[string]any{"pad": strings.Repeat("a", MaxDelegationTokenBytes)}}}), fail(DelegationOversized, 0)), + c("child_scope_outside_parent", "", mint(2, "quote:*", tokenSpec{}, tokenSpec{claims: map[string]any{"scope": "credit:read"}}), fail(DelegationScopeWidened, 2)), + c("child_narrows_wildcard", "publisher:news:read under publisher:*:read", mint(2, "publisher:*:read", tokenSpec{}, tokenSpec{claims: map[string]any{"scope": "publisher:news:read"}}), ok([]string{"publisher:news:read"}, 2)), + c("child_wildcard_widens_parent", "publisher:news:* under publisher:*:read", mint(2, "publisher:*:read", tokenSpec{}, tokenSpec{claims: map[string]any{"scope": "publisher:news:*"}}), fail(DelegationScopeWidened, 2)), + c("child_without_scope", "a child with no scope grants no scoped access and is still valid", mint(2, "quote:*", tokenSpec{}, tokenSpec{claims: map[string]any{"scope": nil}}), ok(nil, 2)), + c("child_exp_later_than_parent", "", mint(2, "quote:*", tokenSpec{}, tokenSpec{claims: map[string]any{"exp": exp + 1}}), fail(DelegationExpiryWidened, 2)), + c("spend_cap_unsupported", "this verifier enforces no cap, so a cap claim is refused rather than dropped", mint(1, "quote:*", tokenSpec{claims: map[string]any{"fora_max_spend_cents": 50000}}), fail(DelegationUnsupportedRestriction, 0)), + c("unknown_claim", "a claim the profile does not list is a restriction the verifier cannot evaluate", mint(1, "quote:*", tokenSpec{claims: map[string]any{"vendor:plan": "gold"}}), fail(DelegationUnknownClaim, 0)), + c("root_cap_unsupported", "a cap on the root under a clean child: claims are checked on every token", mint(2, "quote:*", tokenSpec{claims: map[string]any{"fora_max_spend_cents": 50000}}, tokenSpec{}), fail(DelegationUnsupportedRestriction, 0)), + c("root_unknown_claim", "an unknown claim on the root under a clean child", mint(2, "quote:*", tokenSpec{claims: map[string]any{"vendor:plan": "gold"}}, tokenSpec{}), fail(DelegationUnknownClaim, 0)), + c("middle_cap_unsupported", "a cap on the middle token of three", mint(3, "quote:*", tokenSpec{}, tokenSpec{claims: map[string]any{"fora_max_accesses": 10}}, tokenSpec{}), fail(DelegationUnsupportedRestriction, 0)), + c("middle_unknown_claim", "an unknown claim on the middle token of three", mint(3, "quote:*", tokenSpec{}, tokenSpec{claims: map[string]any{"vendor:plan": "gold"}}, tokenSpec{}), fail(DelegationUnknownClaim, 0)), + c("advisory_unknown_content_ignored", "", mint(1, "quote:*", tokenSpec{claims: map[string]any{"fora_advisory": map[string]any{"vendor": "x", "plan": map[string]any{"tier": 2}}}}), ok([]string{"quote:*"}, 1)), + c("advisory_shadows_profile_claim", "scope inside fora_advisory", mint(1, "quote:*", tokenSpec{claims: map[string]any{"fora_advisory": map[string]any{"scope": "*"}}}), fail(DelegationAdvisoryShadowsClaim, 0)), + with(c("mirrors_disagree_no_effect", "plaintext Delegation fields that disagree with the signed chain change nothing; an Exchange verifies the chain and never reads them for trust", mint(1, "quote:*"), ok([]string{"quote:*"}, 1)), func(e *chainCase) { + e.Mirrors = &chainMirrors{Issuer: "attacker.example", PrincipalDomain: "attacker.example", Scopes: []string{"*"}, ExpiresAt: "2099-01-01T00:00:00Z"} + }), + } + // Raw-byte syntax cases: a valid signature over bytes the JSON marshaller + // never produces, so only the syntax checks can refuse them. + rawHeader := `{"alg":"EdDSA","typ":"fora-delegation+jwt"}` + rawPayload := `{"iss":"` + iss + `","aud":["exchange.example"],"exp":` + fmt.Sprintf("%d", int64(exp)) + `,"scope":"quote:*","cnf":{"jkt":"` + holder + `"}}` + rawGood := signCompactRaw(issuer, rawHeader, rawPayload) + cases = append(cases, + c("raw_baseline", "the raw-signed root every raw syntax case starts from", rawGood, ok([]string{"quote:*"}, 1)), + c("payload_trailing_object", "a second JSON object after the claims object, under a valid signature", signCompactRaw(issuer, rawHeader, rawPayload+` {"scope":"*"}`), fail(DelegationMalformed, 0)), + c("payload_trailing_text", "text after the claims object, under a valid signature", signCompactRaw(issuer, rawHeader, rawPayload+`junk`), fail(DelegationMalformed, 0)), + c("payload_not_object", "a JSON array as payload", signCompactRaw(issuer, rawHeader, `[1]`), fail(DelegationMalformed, 0)), + c("header_b64_false", "RFC 7797 b64:false without crit", signCompactRaw(issuer, `{"alg":"EdDSA","typ":"fora-delegation+jwt","b64":false}`, rawPayload), fail(DelegationMalformed, 0)), + c("signature_crlf", "CR LF appended to the signature segment", rawGood+"\r\n", fail(DelegationMalformed, 0)), + c("signature_padded", "a padding character in the signature segment", rawGood+"=", fail(DelegationMalformed, 0)), + c("exp_beyond_numeric_date_range", "exp 1e100 is finite but no date the profile can return", signCompactRaw(issuer, rawHeader, strings.Replace(rawPayload, fmt.Sprintf("%d", int64(exp)), "1e100", 1)), fail(DelegationBadClaim, 0)), + c("exp_negative", "", signCompactRaw(issuer, rawHeader, strings.Replace(rawPayload, fmt.Sprintf("%d", int64(exp)), "-1", 1)), fail(DelegationBadClaim, 0)), + c("exp_at_numeric_date_max", "253402300799 is the last accepted second", signCompactRaw(issuer, rawHeader, strings.Replace(rawPayload, fmt.Sprintf("%d", int64(exp)), "253402300799", 1)), okAt([]string{"quote:*"}, 1, time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC))), + ) + cases = append(cases, documentedChains(t)...) + return cases +} + +// documentedChains are the chains the documentation embeds as real tokens, with +// the identities, audiences and scopes the pages' stories use. The conformance +// test in conformance/docdelegation_test.go finds every documented token, looks +// its case up here by name, and verifies it with VerifyDelegation under the +// case's inputs, so a page can never show a token that does not verify. +func documentedChains(t *testing.T) []chainCase { + t.Helper() + now := time.Date(2026, 6, 1, 12, 0, 0, 0, time.UTC) + nowStr := now.Format(time.RFC3339) + trust := func(identity string, pub ed25519.PublicKey) []chainTrust { + return []chainTrust{{Identity: identity, KeyX: base64.RawURLEncoding.EncodeToString(pub)}} + } + expect := func(iss, holder string, scopes []string, exp time.Time, depth int) chainExpect { + return chainExpect{Valid: true, RootIssuer: iss, Holder: holder, Scopes: scopes, ExpiresAt: exp.UTC().Format(time.RFC3339), Depth: depth} + } + marketPub, market := delegationTestKey(0xB0) + finbotPub, _ := delegationTestKey(0xB1) + iamPub, iam := delegationTestKey(0xB2) + legalPub, legal := delegationTestKey(0xB3) + subAgentPub, _ := delegationTestKey(0xB4) + hedgePub, _ := delegationTestKey(0xB5) + stanfordPub, stanford := delegationTestKey(0xB6) + litrevPub, _ := delegationTestKey(0xB7) + + hour := now.Add(time.Hour) + yearEnd := time.Date(2026, 12, 31, 23, 59, 59, 0, time.UTC) + return []chainCase{ + { + Name: "docs_subscription_agent", Note: "authentication page, Subscription Agent example: a MarketData root token bound to FinBot's request key", + Token: mintChain(t, []ed25519.PrivateKey{market}, finbotPub, "marketdata.example.com", []string{"exchange.example"}, "subscription:marketdata-2026 quote:* earnings:*", float64(hour.Unix()), []tokenSpec{{claims: map[string]any{"jti": "md-sub-2026-0481"}}}), + Trusted: trust("marketdata.example.com", marketPub), Audience: "exchange.example", Holder: mustThumbprint(t, finbotPub), Now: nowStr, + Expect: expect("marketdata.example.com", mustThumbprint(t, finbotPub), []string{"earnings:*", "quote:*", "subscription:marketdata-2026"}, hour, 1), + }, + { + Name: "docs_enterprise_agent", Note: "authentication page, Enterprise Agent example: an IAM root token bound to the legal research agent's key", + Token: mintChain(t, []ed25519.PrivateKey{iam}, legalPub, "iam.kirkland.com", []string{"exchange.example"}, "legal:case-law legal:statutes credit:read", float64(hour.Unix()), []tokenSpec{{claims: map[string]any{"jti": "iam-7c1e", "sub": "user@kirkland.com"}}}), + Trusted: trust("iam.kirkland.com", iamPub), Audience: "exchange.example", Holder: mustThumbprint(t, legalPub), Now: nowStr, + Expect: expect("iam.kirkland.com", mustThumbprint(t, legalPub), []string{"credit:read", "legal:case-law", "legal:statutes"}, hour, 1), + }, + { + Name: "docs_delegated_sub_agent", Note: "authentication page, Delegated Sub-Agent example: the legal agent narrows the IAM grant to credit:read for a sub-agent, one hour shorter", + Token: mintChain(t, []ed25519.PrivateKey{iam, legal}, subAgentPub, "iam.kirkland.com", []string{"exchange.example"}, "legal:case-law legal:statutes credit:read", float64(hour.Unix()), []tokenSpec{ + {claims: map[string]any{"jti": "iam-7c1e", "sub": "user@kirkland.com"}}, + {alg: "Ed25519", claims: map[string]any{"jti": "legal-sub-0007", "scope": "credit:read", "exp": float64(now.Add(30 * time.Minute).Unix())}}, + }), + Trusted: trust("iam.kirkland.com", iamPub), Audience: "exchange.example", Holder: mustThumbprint(t, subAgentPub), Now: nowStr, + Expect: expect("iam.kirkland.com", mustThumbprint(t, subAgentPub), []string{"credit:read"}, now.Add(30*time.Minute), 2), + }, + { + Name: "docs_walkthrough_v1", Note: "walkthrough v1 scenario B: the MarketData root token for the hedge fund's agent, addressed to exchange.marketdata-data.com", + Token: mintChain(t, []ed25519.PrivateKey{market}, hedgePub, "marketdata.example.com", []string{"exchange.marketdata-data.com"}, "quote:* earnings:* news:read subscription:enterprise-sub-HF-001", float64(yearEnd.Unix()), []tokenSpec{{claims: map[string]any{"jti": "md-hf-001-2026", "sub": "enterprise-sub-HF-001"}}}), + Trusted: trust("marketdata.example.com", marketPub), Audience: "exchange.marketdata-data.com", Holder: mustThumbprint(t, hedgePub), Now: nowStr, + Expect: expect("marketdata.example.com", mustThumbprint(t, hedgePub), []string{"earnings:*", "news:read", "quote:*", "subscription:enterprise-sub-HF-001"}, yearEnd, 1), + }, + { + Name: "docs_walkthrough_academic", Note: "academic walkthrough: Stanford's root token for the literature review agent, addressed to exchange.scholarsgate.com", + Token: mintChain(t, []ed25519.PrivateKey{stanford}, litrevPub, "stanford.edu", []string{"exchange.scholarsgate.com"}, "academic:* subscription:inst-sub-stanford-2026", float64(yearEnd.Unix()), []tokenSpec{{claims: map[string]any{"jti": "inst-sub-stanford-2026-0193", "sub": "inst-sub-stanford-2026"}}}), + Trusted: trust("stanford.edu", stanfordPub), Audience: "exchange.scholarsgate.com", Holder: mustThumbprint(t, litrevPub), Now: nowStr, + Expect: expect("stanford.edu", mustThumbprint(t, litrevPub), []string{"academic:*", "subscription:inst-sub-stanford-2026"}, yearEnd, 1), + }, + } +} + +// tamperPayload flips one character of the outer token's payload so the +// signature no longer covers what is read. +func tamperPayload(t *testing.T, token string) string { + t.Helper() + parts := strings.Split(token, ".") + if len(parts) != 3 { + t.Fatal("not a compact JWS") + } + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + t.Fatal(err) + } + // "quote" → "quotf": still valid JSON and a valid scope, only the bytes differ. + changed := strings.Replace(string(payload), "quote", "quotf", 1) + if changed == string(payload) { + t.Fatal("payload had nothing to tamper") + } + return parts[0] + "." + base64.RawURLEncoding.EncodeToString([]byte(changed)) + "." + parts[2] +} + +func TestGenerateDelegationChains(t *testing.T) { + doc := chainFile{Note: chainFileNote, Cases: buildDelegationChains(t)} + path := filepath.Join("testdata", "delegation-chains.json") + if os.Getenv("FORA_UPDATE_VECTORS") == "1" { + writeJSON(t, path, doc) + return + } + assertMatches(t, path, doc) +} + +// TestDelegationChainsReplay drives the committed file through VerifyDelegation +// and compares with the declared outcome of every case. +func TestDelegationChainsReplay(t *testing.T) { + raw, err := os.ReadFile(filepath.Join("testdata", "delegation-chains.json")) + if err != nil { + t.Fatal(err) + } + var doc chainFile + if err := json.Unmarshal(raw, &doc); err != nil { + t.Fatal(err) + } + if len(doc.Cases) == 0 { + t.Fatal("no cases") + } + var passed, refused int + for _, c := range doc.Cases { + t.Run(c.Name, func(t *testing.T) { + in := DelegationInput{Token: []byte(c.Token), Audience: c.Audience, Holder: c.Holder} + in.Now, err = time.Parse(time.RFC3339, c.Now) + if err != nil { + t.Fatal(err) + } + for _, tr := range c.Trusted { + x, err := base64.RawURLEncoding.DecodeString(tr.KeyX) + if err != nil { + t.Fatal(err) + } + in.TrustedIssuers = append(in.TrustedIssuers, TrustedIssuer{Identity: tr.Identity, Key: ed25519.PublicKey(x)}) + } + got, err := VerifyDelegation(in) + if !c.Expect.Valid { + refused++ + var de *DelegationError + if !errors.As(err, &de) { + t.Fatalf("expected refusal %s, got %v (%+v)", c.Expect.Failure, err, got) + } + if string(de.Failure) != c.Expect.Failure { + t.Fatalf("expected failure %s, got %s: %v", c.Expect.Failure, de.Failure, err) + } + if de.Token != c.Expect.Token { + t.Fatalf("expected the refusal on token %d, got token %d: %v", c.Expect.Token, de.Token, err) + } + return + } + passed++ + if err != nil { + t.Fatalf("expected success, got %v", err) + } + if got.RootIssuer != c.Expect.RootIssuer || got.Holder != c.Expect.Holder || got.Depth != c.Expect.Depth || + strings.Join(got.Scopes, " ") != strings.Join(c.Expect.Scopes, " ") || + got.ExpiresAt.UTC().Format(time.RFC3339) != c.Expect.ExpiresAt { + t.Fatalf("result %+v does not match the declared outcome %+v", got, c.Expect) + } + }) + } + if passed == 0 || refused == 0 { + t.Fatalf("cases must carry both outcomes; got %d/%d", passed, refused) + } +} diff --git a/sdk/go/helpers/gen_util_vectors_test.go b/sdk/go/helpers/gen_util_vectors_test.go index a4ff6d03..9fa81cb7 100644 --- a/sdk/go/helpers/gen_util_vectors_test.go +++ b/sdk/go/helpers/gen_util_vectors_test.go @@ -42,8 +42,10 @@ type scopesNormalizeVector struct { Normalized []string `json:"normalized"` } -// scopesSubsetVector is one ScopesSubset case: (sub ⊆ super) as the REAL -// ScopesSubset computes it. Empty sub ⇒ true. +// scopesSubsetVector is one ScopesSubset case: whether every scope in sub is +// covered by some scope in super under the segment-wise matching rule, as the +// REAL ScopesSubset computes it. Empty sub ⇒ true. The single-pair rule itself +// is pinned by the hand-written scope-match-vectors.json. type scopesSubsetVector struct { Name string `json:"name"` Sub []string `json:"sub"` @@ -113,7 +115,10 @@ func buildScopesNormalizeVectors() []scopesNormalizeVector { } // buildScopesSubsetVectors emits the ScopesSubset corpus, deriving expected from -// the REAL ScopesSubset (sub ⊆ super; empty sub ⇒ true). +// the REAL ScopesSubset (every sub scope covered by some super scope; empty +// sub ⇒ true). The exact-string cases keep their old verdicts under the +// containment rule; the wildcard cases are what separate containment from +// set membership. func buildScopesSubsetVectors() []scopesSubsetVector { cases := []struct { name string @@ -127,6 +132,12 @@ func buildScopesSubsetVectors() []scopesSubsetVector { {"partial_overlap_not_subset", []string{"read", "admin"}, []string{"read", "write"}}, {"sub_nonempty_super_empty", []string{"read"}, nil}, {"case_sensitive_not_subset", []string{"Read"}, []string{"read"}}, + {"wildcard_super_covers_narrower_sub", []string{"publisher:news:read"}, []string{"publisher:*:read"}}, + {"sub_wildcard_wider_than_super", []string{"publisher:news:*"}, []string{"publisher:*:read"}}, + {"terminal_wildcard_super_covers_deeper_sub", []string{"dist:US:CA"}, []string{"dist:*"}}, + {"sub_bare_star_not_covered_by_narrow_super", []string{"*"}, []string{"read"}}, + {"no_implicit_prefix_in_subset", []string{"dist:US"}, []string{"dist"}}, + {"sub_longer_than_super_terminal_star", []string{"dist:US"}, []string{"dist:US:*"}}, } out := make([]scopesSubsetVector, 0, len(cases)) for _, c := range cases { diff --git a/sdk/go/helpers/scope_match_vectors_test.go b/sdk/go/helpers/scope_match_vectors_test.go new file mode 100644 index 00000000..f0386b9f --- /dev/null +++ b/sdk/go/helpers/scope_match_vectors_test.go @@ -0,0 +1,62 @@ +package helpers + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" +) + +// scopeCoverVector is one ScopeCovers case: a granted scope, a required scope +// and the verdict the WRITTEN scope-matching rule (Requester.scopes) gives. +type scopeCoverVector struct { + Name string `json:"name"` + Granted string `json:"granted"` + Required string `json:"required"` + Expected bool `json:"expected"` +} + +// scopeCoverSetVector is one ScopesCover case over lists. The same row also +// pins ScopesSubset(required, granted), which is the same containment check +// read from the child's side. +type scopeCoverSetVector struct { + Name string `json:"name"` + Granted []string `json:"granted"` + Required []string `json:"required"` + Expected bool `json:"expected"` +} + +// TestScopeMatchCorpusReplays drives every row of scope-match-vectors.json +// through ScopeCovers, ScopesCover and ScopesSubset. Unlike the emitted +// corpora, this file is hand-written from the rule text so the verdicts do not +// derive from the implementation they test; Python and TypeScript replay the +// same file. +func TestScopeMatchCorpusReplays(t *testing.T) { + raw, err := os.ReadFile(filepath.Join("testdata", "scope-match-vectors.json")) + if err != nil { + t.Fatalf("read corpus: %v", err) + } + var doc struct { + Covers []scopeCoverVector `json:"covers"` + CoverSets []scopeCoverSetVector `json:"cover_sets"` + } + if err := json.Unmarshal(raw, &doc); err != nil { + t.Fatalf("decode corpus: %v", err) + } + if len(doc.Covers) == 0 || len(doc.CoverSets) == 0 { + t.Fatal("corpus must carry covers and cover_sets rows") + } + for _, c := range doc.Covers { + if got := ScopeCovers(c.Granted, c.Required); got != c.Expected { + t.Errorf("%s: ScopeCovers(%q, %q) = %v, corpus says %v", c.Name, c.Granted, c.Required, got, c.Expected) + } + } + for _, c := range doc.CoverSets { + if got := ScopesCover(c.Granted, c.Required); got != c.Expected { + t.Errorf("%s: ScopesCover(%q, %q) = %v, corpus says %v", c.Name, c.Granted, c.Required, got, c.Expected) + } + if got := ScopesSubset(c.Required, c.Granted); got != c.Expected { + t.Errorf("%s: ScopesSubset(%q, %q) = %v, corpus says %v", c.Name, c.Required, c.Granted, got, c.Expected) + } + } +} diff --git a/sdk/go/helpers/scopes.go b/sdk/go/helpers/scopes.go index 98462a16..c6fdeea6 100644 --- a/sdk/go/helpers/scopes.go +++ b/sdk/go/helpers/scopes.go @@ -2,6 +2,7 @@ package helpers import ( "sort" + "strings" forav1 "github.com/FORA-Protocol/protocol/gen/go/fora/v1" ) @@ -54,19 +55,67 @@ func ApplyScopes(r *forav1.Requester, scopes ...string) { r.Scopes = NormalizeScopes(scopes) } -// ScopesSubset reports whether every scope in sub is present in super. It is the -// delegation-attenuation rule (Delegation.scopes MUST be a subset of the -// principal's granted scopes): use it to validate a delegation before relying on -// it, so an over-broad delegation is caught at the SDK boundary. -func ScopesSubset(sub, super []string) bool { - set := make(map[string]struct{}, len(super)) - for _, s := range super { - set[s] = struct{}{} +// ScopeCovers reports whether one granted scope covers one required scope under +// the protocol's scope-matching rule (the rule stated on Requester.scopes, which +// LicenseTerm.scopes and delegation attenuation share): +// +// - both scopes are split on ":" into segments; +// - each granted segment must equal the required segment at the same position +// or be "*"; +// - a "*" in the last position of the grant covers every remaining required +// segment, and there must be at least one; +// - there is no implicit prefix match: "dist" covers only "dist", not "dist:US"; +// - a grant with more segments than the requirement is narrower and never +// covers it, even when its last segment is "*" ("dist:US:*" does not cover +// "dist:US"); +// - a "*" inside the requirement is a literal segment, so a narrow grant never +// covers a wildcard requirement ("credit:read" does not cover "*"); +// - an empty string covers nothing and is covered by nothing. +// +// Matching is case-sensitive and never trims. +func ScopeCovers(granted, required string) bool { + if granted == "" || required == "" { + return false } - for _, s := range sub { - if _, ok := set[s]; !ok { + g := strings.Split(granted, ":") + r := strings.Split(required, ":") + for i, seg := range g { + if seg == "*" && i == len(g)-1 { + return i < len(r) + } + if i >= len(r) || (seg != "*" && seg != r[i]) { + return false + } + } + return len(g) == len(r) +} + +// ScopesCover reports whether every scope in required is covered (ScopeCovers) +// by at least one scope in granted. An empty required list is always covered; +// an empty granted list covers nothing but an empty required list. +func ScopesCover(granted, required []string) bool { + for _, req := range required { + covered := false + for _, g := range granted { + if ScopeCovers(g, req) { + covered = true + break + } + } + if !covered { return false } } return true } + +// ScopesSubset reports whether every scope in sub is covered by some scope in +// super under ScopeCovers (grant containment, not exact set membership). It is +// the delegation-attenuation rule: a child's scopes must be contained in its +// parent's grant. A child may narrow a wildcard ("publisher:news:read" under +// "publisher:*:read") but may not widen one ("publisher:news:*" under +// "publisher:*:read" is refused). ScopesSubset(sub, super) is +// ScopesCover(super, sub) read from the child's side. +func ScopesSubset(sub, super []string) bool { + return ScopesCover(super, sub) +} diff --git a/sdk/go/helpers/scopes_test.go b/sdk/go/helpers/scopes_test.go index 004ffb9f..fa5740aa 100644 --- a/sdk/go/helpers/scopes_test.go +++ b/sdk/go/helpers/scopes_test.go @@ -42,4 +42,25 @@ func TestScopesSubset(t *testing.T) { if !helpers.ScopesSubset(nil, granted) { t.Error("empty subset is always a subset") } + if !helpers.ScopesSubset([]string{"publisher:news:read"}, []string{"publisher:*:read"}) { + t.Error("a child may narrow a parent wildcard") + } + if helpers.ScopesSubset([]string{"publisher:news:*"}, []string{"publisher:*:read"}) { + t.Error("a child may not widen the parent grant") + } +} + +func TestScopeCovers(t *testing.T) { + if !helpers.ScopeCovers("dist:*", "dist:US:CA") { + t.Error("terminal wildcard covers all remaining segments") + } + if helpers.ScopeCovers("dist", "dist:US") { + t.Error("no implicit prefix match") + } + if !helpers.ScopesCover([]string{"dist:*", "credit:read"}, []string{"dist:US", "credit:read"}) { + t.Error("every required scope is covered by some grant") + } + if helpers.ScopesCover(nil, []string{"credit:read"}) { + t.Error("an empty grant covers nothing") + } } diff --git a/sdk/go/helpers/testdata/delegation-chains.json b/sdk/go/helpers/testdata/delegation-chains.json new file mode 100644 index 00000000..6b9d4274 --- /dev/null +++ b/sdk/go/helpers/testdata/delegation-chains.json @@ -0,0 +1,1626 @@ +{ + "note": "Signed delegation chain cases with declared outcomes; regenerate with FORA_UPDATE_VECTORS=1 in sdk/go/helpers. expect.token_index is 0 when the refusal is found while decoding, before the chain depth is known (size, syntax, header and claim checks), otherwise the root-relative position, 1 for the root. mirrors are the plaintext Delegation fields next to the token; only an Exchange's own tests replay them, nothing in the protocol repository reads them.", + "cases": [ + { + "name": "root_only_eddsa", + "note": "one token, alg EdDSA, holder bound to the request key", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImlzcyI6Im1hcmtldGRhdGEuZXhhbXBsZSIsInNjb3BlIjoicXVvdGU6KiBlYXJuaW5nczoqIn0.afiAcjLPi8aGlp_PaOQvXz9C7e5owt0IdWwH8TgOZqs3EGf1owmj4YHt8Cd40DoI1aB-whoY2jgGf4zy3HiHCQ", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": true, + "root_issuer": "marketdata.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "scopes": [ + "earnings:*", + "quote:*" + ], + "expires_at": "2026-06-01T13:00:00Z", + "depth": 1 + } + }, + { + "name": "root_only_ed25519", + "note": "one token, alg Ed25519 (RFC 9864)", + "token": "eyJhbGciOiJFZDI1NTE5IiwidHlwIjoiZm9yYS1kZWxlZ2F0aW9uK2p3dCJ9.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImlzcyI6Im1hcmtldGRhdGEuZXhhbXBsZSIsInNjb3BlIjoicXVvdGU6KiJ9.b-if3xMuuuTPwgOFQ3C0GHXVdLSPCTUy1H2BPOWdU_R0UnpoO5aZL_aG_76LpPV4TB3qkZhXDC4uIBIusxSJDA", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": true, + "root_issuer": "marketdata.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "scopes": [ + "quote:*" + ], + "expires_at": "2026-06-01T13:00:00Z", + "depth": 1 + } + }, + { + "name": "two_tokens", + "note": "root EdDSA, child Ed25519 narrowing scope", + "token": "eyJhbGciOiJFZDI1NTE5IiwiandrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoieUYwU2RxTHZHZnlIVjRMV2hMTS1nTXg3clBnX0xNeFpLcHBKZFk3blJ2WSJ9LCJ0eXAiOiJmb3JhLWRlbGVnYXRpb24rand0In0.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImZvcmFfcGFyZW50IjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNkltWnZjbUV0WkdWc1pXZGhkR2x2Yml0cWQzUWlmUS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVaWGhoYlhCc1pTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaVdHUjNZWEpFTkV3d01WQlpObEZhTUhSaE16QnBia0pNUjNaVU5UZHBkRTFVV1Vsa01raDZRM0ZTUlNKOUxDSmxlSEFpT2pFM09EQXpNVGc0TURBc0ltbHpjeUk2SW0xaGNtdGxkR1JoZEdFdVpYaGhiWEJzWlNJc0luTmpiM0JsSWpvaWNYVnZkR1U2S2lCbFlYSnVhVzVuY3pvcUluMC5pa0tTX2tzVkZTSzk2eE9ja2ZjMW8zc2prd1VpUUhMSUFpNFpCM3B1eHdmMWJQOTgzcGl6RkhLNy16Q2pid2VLVmNWaFczeTlkOEEyTTJnVGxtTk5BUSIsImlzcyI6Ilhkd2FyRDRMMDFQWTZRWjB0YTMwaW5CTEd2VDU3aXRNVFlJZDJIekNxUkUiLCJzY29wZSI6ImVhcm5pbmdzOioifQ.BYPx8_MNws93olALVra3qZDnx-Xw9wIhtansjrBcAJzEid1psR764LvcyE0pdJ1GhXfuMd9SYWRHQHRy6fOoCA", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": true, + "root_issuer": "marketdata.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "scopes": [ + "earnings:*" + ], + "expires_at": "2026-06-01T13:00:00Z", + "depth": 2 + } + }, + { + "name": "two_tokens_mixed_reverse", + "note": "root Ed25519, child EdDSA", + "token": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6InlGMFNkcUx2R2Z5SFY0TFdoTE0tZ014N3JQZ19MTXhaS3BwSmRZN25SdlkifSwidHlwIjoiZm9yYS1kZWxlZ2F0aW9uK2p3dCJ9.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImZvcmFfcGFyZW50IjoiZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpZEhsd0lqb2labTl5WVMxa1pXeGxaMkYwYVc5dUsycDNkQ0o5LmV5SmhkV1FpT2xzaVpYaGphR0Z1WjJVdVpYaGhiWEJzWlNKZExDSmpibVlpT25zaWFtdDBJam9pV0dSM1lYSkVORXd3TVZCWk5sRmFNSFJoTXpCcGJrSk1SM1pVTlRkcGRFMVVXVWxrTWtoNlEzRlNSU0o5TENKbGVIQWlPakUzT0RBek1UZzRNREFzSW1semN5STZJbTFoY210bGRHUmhkR0V1WlhoaGJYQnNaU0lzSW5OamIzQmxJam9pY1hWdmRHVTZLaUo5LnRLSktnejhlVC16MVhNeWg0QVVTb1pIengyeGRINk94OUx4ZlRRSERUMGNzWVE3djhWOHRMSE5VaXB3eW9WX0dNSDBybloyaVpEMExwcks5MkxlckR3IiwiaXNzIjoiWGR3YXJENEwwMVBZNlFaMHRhMzBpbkJMR3ZUNTdpdE1UWUlkMkh6Q3FSRSIsInNjb3BlIjoicXVvdGU6KiJ9.JvfjbpFHXDFsSdOnE-FHhy6jq7mAZenEkvsmxfMdGgoxg9dfl2F-XBUovPc795qsey83iJcb3mE0tWrmQNphBg", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": true, + "root_issuer": "marketdata.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "scopes": [ + "quote:*" + ], + "expires_at": "2026-06-01T13:00:00Z", + "depth": 2 + } + }, + { + "name": "three_tokens", + "note": "root, principal, agent", + "token": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im8wOEhqQV9XOTM5Y2JjdWxNUkpNc243d3FUSkFQNnZKaWJ6YTlEejl0UTQifSwidHlwIjoiZm9yYS1kZWxlZ2F0aW9uK2p3dCJ9.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImZvcmFfcGFyZW50IjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJbXAzYXlJNmV5SmpjbllpT2lKRlpESTFOVEU1SWl3aWEzUjVJam9pVDB0UUlpd2llQ0k2SW5sR01GTmtjVXgyUjJaNVNGWTBURmRvVEUwdFowMTROM0pRWjE5TVRYaGFTM0J3U21SWk4yNVNkbGtpZlN3aWRIbHdJam9pWm05eVlTMWtaV3hsWjJGMGFXOXVLMnAzZENKOS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVaWGhoYlhCc1pTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaU5YUkNSRk4zVjNCM1JWOWthbUZaY1d4clFtUlNUa1o2VGpaSFozcEhTbWxYYnpSaFpVazRZa1JXVlNKOUxDSmxlSEFpT2pFM09EQXpNVGc0TURBc0ltWnZjbUZmY0dGeVpXNTBJam9pWlhsS2FHSkhZMmxQYVVwR1drVlNWRkZUU1hOSmJsSTFZME5KTmtsdFduWmpiVVYwV2tkV2MxcFhaR2hrUjJ4MlltbDBjV1F6VVdsbVVTNWxlVXBvWkZkUmFVOXNjMmxhV0docVlVZEdkVm95VlhWYVdHaG9ZbGhDYzFwVFNtUk1RMHBxWW0xWmFVOXVjMmxoYlhRd1NXcHZhVmRIVWpOWldFcEZUa1YzZDAxV1FscE9iRVpoVFVoU2FFMTZRbkJpYTBwTlVqTmFWVTVVWkhCa1JURlZWMVZzYTAxcmFEWlJNMFpUVWxOS09VeERTbXhsU0VGcFQycEZNMDlFUVhwTlZHYzBUVVJCYzBsdGJIcGplVWsyU1cweGFHTnRkR3hrUjFKb1pFZEZkVnBZYUdoaVdFSnpXbE5KYzBsdVRtcGlNMEpzU1dwdmFXTllWblprUjFVMlMybEtPUzV2YTBkSWFuQm9NSEZDWlRGdk1uWlNObVo0VW1FeVJrZFNjbmgwUWkxaFZIWm5UVTF5T0RCWlFYbGlWMGhwTWpScE9FaHZMVVpuU3pSeFdUbDZaR1pVVjBscmFqaE5lbTFqUm5oVFdHNTNlRkJEVEc5Qlp5SXNJbWx6Y3lJNklsaGtkMkZ5UkRSTU1ERlFXVFpSV2pCMFlUTXdhVzVDVEVkMlZEVTNhWFJOVkZsSlpESklla054VWtVaUxDSnpZMjl3WlNJNkluRjFiM1JsT2lvaWZRLlVwVk92WmI4MGFiTnJKTVM4bVprVnF6UHlWbTBCU2pla1RCUURKdS1rT0dnQVpMM2hkRi1sTGhxMEU4N0M1TExEWnByb190bDRuNDAyaGNsOUNtNUNBIiwiaXNzIjoiNXRCRFN3V3B3RV9kamFZcWxrQmRSTkZ6TjZHZ3pHSmlXbzRhZUk4YkRWVSIsInNjb3BlIjoicXVvdGU6QUFQTCJ9.Uk_05oKsgURkQfu7RpRcz50dbwx0nAuhKHIj6SEX_aW7fib_S70axwe5Zrm1kicvXBZOUfGZwEDO9OO4A-5VAQ", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": true, + "root_issuer": "marketdata.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "scopes": [ + "quote:AAPL" + ], + "expires_at": "2026-06-01T13:00:00Z", + "depth": 3 + } + }, + { + "name": "four_tokens", + "note": "the deepest chain the profile allows", + "token": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6InZ1bzlQNXl0WEZXQ1l0alBVZTZndGhPQ0tjb3ZJd1ZUaExEOEZuWlREQkUifSwidHlwIjoiZm9yYS1kZWxlZ2F0aW9uK2p3dCJ9.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImZvcmFfcGFyZW50IjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJbXAzYXlJNmV5SmpjbllpT2lKRlpESTFOVEU1SWl3aWEzUjVJam9pVDB0UUlpd2llQ0k2SW04d09FaHFRVjlYT1RNNVkySmpkV3hOVWtwTmMyNDNkM0ZVU2tGUU5uWkthV0o2WVRsRWVqbDBVVFFpZlN3aWRIbHdJam9pWm05eVlTMWtaV3hsWjJGMGFXOXVLMnAzZENKOS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVaWGhoYlhCc1pTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaVJuSXRUbU5IYWtaNVNUWmhOazR6VUVKdlJrb3lOM04yUjJWdFdubGtOSFZuTUVkc1ZWOWxUMjFOT0NKOUxDSmxlSEFpT2pFM09EQXpNVGc0TURBc0ltWnZjbUZmY0dGeVpXNTBJam9pWlhsS2FHSkhZMmxQYVVwR1drVlNWRkZUU1hOSmJYQXpZWGxKTm1WNVNtcGpibGxwVDJsS1JscEVTVEZPVkVVMVNXbDNhV0V6VWpWSmFtOXBWREIwVVVscGQybGxRMGsyU1c1c1IwMUdUbXRqVlhneVVqSmFOVk5HV1RCVVJtUnZWRVV3ZEZvd01UUk9NMHBSV2pFNVRWUllhR0ZUTTBKM1UyMVNXazR5TlZOa2JHdHBabE4zYVdSSWJIZEphbTlwV20wNWVWbFRNV3RhVjNoc1dqSkdNR0ZYT1hWTE1uQXpaRU5LT1M1bGVVcG9aRmRSYVU5c2MybGFXR2hxWVVkR2RWb3lWWFZhV0dob1lsaENjMXBUU21STVEwcHFZbTFaYVU5dWMybGhiWFF3U1dwdmFVNVlVa05TUms0elZqTkNNMUpXT1d0aGJVWmFZMWQ0Y2xGdFVsTlVhMW8yVkdwYVNGb3pjRWhUYld4WVlucFNhRnBWYXpSWmExSlhWbE5LT1V4RFNteGxTRUZwVDJwRk0wOUVRWHBOVkdjMFRVUkJjMGx0V25aamJVWm1ZMGRHZVZwWE5UQkphbTlwV2xoc1MyRkhTa2haTW14UVlWVndSMWRyVmxOV1JrWlVVMWhPU21Kc1NURlpNRTVLVG10c2RGZHVXbXBpVlZZd1YydGtWMk14Y0ZoYVIyaHJVako0TWxsdGJEQmpWMUY2VlZkc2JWVlROV3hsVlhCdldrWmtVbUZWT1hOak1teGhWMGRvY1ZsVlpFZGtWbTk1VmxoV1lWZEhhRzlaYkdoRFl6RndWRk50VWsxUk1IQnhXVzB4V21GVk9YVmpNbXhvWWxoUmQxTlhjSFpoVm1SSVZXcE9XbGRGY0VaVWExWXpaREF4VjFGc2NFOWlSVnBvVkZWb1UyRkZNVFpSYmtKcFlUQndUbFZxVG1GV1ZUVlZXa2hDYTFKVVJsWldNVlp6WVRBeGNtRkVXbEpOTUZwVVZXeE9TMDlWZUVSVGJYaHNVMFZHY0ZReWNFWk5NRGxGVVZod1RsWkhZekJVVlZKQ1l6QnNkR0pJY0dwbFZXc3lVMWN3ZUdGSFRuUmtSM2hyVWpGS2IxcEZaRVprVm5CWllVZG9hVmRGU25wWGJFNUtZekJzZFZSdGNHbE5NRXB6VTFkd2RtRlhUbGxXYmxwclVqRlZNbE15YkV0UFV6VjJZVEJrU1dGdVFtOU5TRVpEV2xSR2RrMXVXbE5PYlZvMFZXMUZlVkpyWkZOamJtZ3dVV2t4YUZaSVdtNVVWVEY1VDBSQ1dsRlliR2xXTUdod1RXcFNjRTlGYUhaTVZWcHVVM3BTZUZkVWJEWmFSMXBWVmpCc2NtRnFhRTVsYlRGcVVtNW9WRmRITlRObFJrSkVWRWM1UWxwNVNYTkpiV3g2WTNsSk5rbHNhR3RrTWtaNVVrUlNUVTFFUmxGWFZGcFNWMnBDTUZsVVRYZGhWelZEVkVWa01sWkVWVE5oV0ZKT1ZrWnNTbHBFU2tsbGEwNTRWV3RWYVV4RFNucFpNamwzV2xOSk5rbHVSakZpTTFKc1QybHZhV1pSTGxWd1ZrOTJXbUk0TUdGaVRuSktUVk00YlZwclZuRjZVSGxXYlRCQ1UycGxhMVJDVVVSS2RTMXJUMGRuUVZwTU0yaGtSaTFzVEdoeE1FVTROME0xVEV4RVduQnliMTkwYkRSdU5EQXlhR05zT1VOdE5VTkJJaXdpYVhOeklqb2lOWFJDUkZOM1YzQjNSVjlrYW1GWmNXeHJRbVJTVGtaNlRqWkhaM3BIU21sWGJ6UmhaVWs0WWtSV1ZTSXNJbk5qYjNCbElqb2ljWFZ2ZEdVNktpSjkuTDZhbDhHVlUwSVJLLVNLOXFRX3FDdXMxUm5mbGtzaTFyZjNORVQ2UnNuekRoN3k2NjZkWWpFUHRudTlfckhRS05vX2Zhek9ySDVraDRxUzdCaEtMQ3ciLCJpc3MiOiJGci1OY0dqRnlJNmE2TjNQQm9GSjI3c3ZHZW1aeWQ0dWcwR2xVX2VPbU04Iiwic2NvcGUiOiJxdW90ZTpBQVBMIn0.0q35vRGpFUD3YjHZZ-ipN4pNkXPp0wDFnrS9dqnHc1fLrAHHm8efb1LD3kL-DYVXF3oUltMY1ORCnbjlcYRnDw", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": true, + "root_issuer": "marketdata.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "scopes": [ + "quote:AAPL" + ], + "expires_at": "2026-06-01T13:00:00Z", + "depth": 4 + } + }, + { + "name": "five_tokens", + "note": "a fifth token is never decoded", + "token": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6ImV4NmlHdzZzVUE0cXB5Y2hQajBEZFRzTFVBTzRfYzk1Sl90bl9kYkREeDQifSwidHlwIjoiZm9yYS1kZWxlZ2F0aW9uK2p3dCJ9.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImZvcmFfcGFyZW50IjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJbXAzYXlJNmV5SmpjbllpT2lKRlpESTFOVEU1SWl3aWEzUjVJam9pVDB0UUlpd2llQ0k2SW5aMWJ6bFFOWGwwV0VaWFExbDBhbEJWWlRabmRHaFBRMHRqYjNaSmQxWlVhRXhFT0VadVdsUkVRa1VpZlN3aWRIbHdJam9pWm05eVlTMWtaV3hsWjJGMGFXOXVLMnAzZENKOS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVaWGhoYlhCc1pTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaVNVcHpjelpWVlZWeFFqbHJWVlpvVWxkbU4zcEViMnhHYlhsdFMzTnNkbTV6UjB0Zk4xRmxVVlp6WXlKOUxDSmxlSEFpT2pFM09EQXpNVGc0TURBc0ltWnZjbUZmY0dGeVpXNTBJam9pWlhsS2FHSkhZMmxQYVVwR1drVlNWRkZUU1hOSmJYQXpZWGxKTm1WNVNtcGpibGxwVDJsS1JscEVTVEZPVkVVMVNXbDNhV0V6VWpWSmFtOXBWREIwVVVscGQybGxRMGsyU1cwNGQwOUZhSEZSVmpsWVQxUk5OVmt5U21wa1YzaE9WV3R3VG1NeU5ETmtNMFpWVTJ0R1VVNXVXa3RoVjBvMldWUnNSV1ZxYkRCVlZGRnBabE4zYVdSSWJIZEphbTlwV20wNWVWbFRNV3RhVjNoc1dqSkdNR0ZYT1hWTE1uQXpaRU5LT1M1bGVVcG9aRmRSYVU5c2MybGFXR2hxWVVkR2RWb3lWWFZhV0dob1lsaENjMXBUU21STVEwcHFZbTFaYVU5dWMybGhiWFF3U1dwdmFWSnVTWFJVYlU1SVlXdGFOVk5VV21oT2F6UjZWVVZLZGxKcmIzbE9NMDR5VWpKV2RGZHViR3RPU0ZadVRVVmtjMVpXT1d4VU1qRk9UME5LT1V4RFNteGxTRUZwVDJwRk0wOUVRWHBOVkdjMFRVUkJjMGx0V25aamJVWm1ZMGRHZVZwWE5UQkphbTlwV2xoc1MyRkhTa2haTW14UVlWVndSMWRyVmxOV1JrWlVVMWhPU21KWVFYcFpXR3hLVG0xV05WTnRjR3BpYkd4d1ZESnNTMUpzY0VWVFZFWlBWa1ZWTVZOWGJETmhWMFY2VldwV1NtRnRPWEJXUkVJd1ZWVnNjR1F5Ykd4Uk1Hc3lVMWMxYzFJd01VZFViWFJxVmxobmVWVnFTbUZPVms1SFYxUkNWVkp0VW5aV1JWVjNaRVp2ZDAxVVVrOU5NSEJTVjJwRk5WUldVbGxoUjBaVVRUQktNMVV5TVZOWGF6UjVUbFpPYTJKSGRIQmFiRTR6WVZkU1NXSklaRXBoYlRsd1YyMHdOV1ZXYkZSTlYzUmhWak5vYzFkcVNrZE5SMFpZVDFoV1RFMXVRWHBhUlU1TFQxTTFiR1ZWY0c5YVJtUlNZVlU1YzJNeWJHRlhSMmh4V1ZWa1IyUldiM2xXV0ZaaFYwZG9iMWxzYUVOak1YQlVVMjFTVFZFd2NIRlpiVEZhWVZVNWRXTXliR2hpV0ZGM1UxZHdkbUZWTlZsVmEwNVRVbXMwZWxacVRrTk5NVXBYVDFkMGFHSlZXbUZaTVdRMFkyeEdkRlZzVGxWaE1XOHlWa2R3WVZOR2IzcGpSV2hVWWxkNFdWbHVjRk5oUm5CV1lYcFNXbUV4U2xoV2JFNUxUMVY0UkZOdGVHeFRSVVp3VkRKd1JrMHdPVVZSV0hCT1ZrZGpNRlJWVWtKak1HeDBWMjVhYW1KVldtMVpNR1JIWlZad1dFNVVRa3BoYlRsd1YyeG9jMU15UmtoVGEyaGFUVzE0VVZsV1ZuZFNNV1J5Vm14T1YxSnJXbFZWTVdoUFUyMUtjMU5VUmxwTlJUVkxWRzEwYzJSR1pIVlhiWEJwVmxaWmQxWXlkR3RXTWsxNFkwWm9ZVkl5YUhKVmFrbzBUV3hzZEdKRVFtcFdNVVkyVmxaa2MySldWbFJPVjNoc1ZsaENkbGRyV210VmJVWldUMWhPYWsxdGVHaFdNR1J2WTFac1ZscEZaR3RXYlRrMVZteG9WMWxXWkVoaFJ6bGFZa2RvUkZsNlJuZFdSazUwVldzeFVrMUlRbmhYVnpCNFYyMUdWazlZVm1wTmJYaHZXV3hvVW1ReFRsaGpTRnBvVm0xU1NWWlhjRTlYYkdSR1kwVmFWV0V4V1hwYVJFRjRWakZHYzJORk9XbFNWbkJ2VmtaV2IxVXlSa1pOVkZwU1ltdEtjRmxVUW5kVWJGWnhWRzFHVjFaVVZsWlhhMmhEWVRGS1ZWSnNXbGROVmxwNldWUkJlR050UmtWWGJFcE9UVVp3VlZaWGVFOVRNRGxXWlVWU1ZHSllhSE5WTUZaSFkwWlJlV05GV2s1TlJHeEdWVlpvZDFSc1draFpla0pWVmxaS1ExbDZRbk5rUjBwSlkwZHdiRlpYYzNsVk1XTjNaVWRHU0ZSdVVtdFNNMmh5VldwR1MySXhjRVphUlZwclZtNUNXbGxWWkc5aFZtUkdVMjV3V0dKRk5VdFpla0p6WkZaU2RHTkhiRTVOUlhCNlZURmtkMlJ0UmxoVWJHeFhZbXh3Y2xWcVJsWk5iRTE1WWtWMFVGVjZWakpaVkVKclUxZEdkVkZ0T1U1VFJWcEVWMnhTUjJSck1YVlhiRTVQWWxadk1GWlhNVVpsVmtweVdrWk9hbUp0WjNkVlYydDRZVVphU1ZkdE5WVldWRVkxVkRCU1ExZHNSbGxpUjJ4WFRVZG9kMVJYY0ZOalJUbEdZVWhhVFZaV2NIVlZNM0JUWlVaa1ZXSkVXbUZTTVhCV1ZtcENjMk50Um5GaFJUVnNZbFJHY1ZWdE5XOVdSbVJJVGxST2JGSnJTa1ZXUldNMVVXeHdOVk5ZVGtwaVYzZzJXVE5zU2s1cmJITmhSM1JyVFd0YU5WVnJVbE5VVlRGRlVteEdXRlpHY0ZOV01uQkRUVVpzVlZSWVpHaFdlbFpFVmtWV2EwMXNXa1ZXVkU1b1YwWktUMVpyV25OVGJIQkZVMnRzYkdFd05UUldWM1JXWVZWNFJGTnVjRnBOYW13elYyeE9TazVyYkhWU2FrWnBUVEZLYzFReWJIWmhWMXBTVEd4V2QxWnJPVEpYYlVrMFRVZEdhVlJ1U2t0VVZrMDBZbFp3Y2xadVJqWlZTR3hYWWxSQ1ExVXljR3hoTVZKRFZWVlNTMlJUTVhKVU1HUnVVVlp3VFUweWFHdFNhVEZ6VkVkb2VFMUZWVFJPTUUweFZFVjRSVmR1UW5saU1Ua3dZa1JTZFU1RVFYbGhSMDV6VDFWT2RFNVZUa0pKYVhkcFlWaE9la2xxYjJsT1dGSkRVa1pPTTFZelFqTlNWamxyWVcxR1dtTlhlSEpSYlZKVFZHdGFObFJxV2toYU0zQklVMjFzV0dKNlVtaGFWV3MwV1d0U1YxWlRTWE5KYms1cVlqTkNiRWxxYjJsaldGWjJaRWRWTmt0cFNqa3VURFpoYkRoSFZsVXdTVkpMTFZOTE9YRlJYM0ZEZFhNeFVtNW1iR3R6YVRGeVpqTk9SVlEyVW5OdWVrUm9OM2syTmpaa1dXcEZVSFJ1ZFRsZmNraFJTMDV2WDJaaGVrOXlTRFZyYURSeFV6ZENhRXRNUTNjaUxDSnBjM01pT2lKR2NpMU9ZMGRxUm5sSk5tRTJUak5RUW05R1NqSTNjM1pIWlcxYWVXUTBkV2N3UjJ4VlgyVlBiVTA0SWl3aWMyTnZjR1VpT2lKeGRXOTBaVG9xSW4wLjJCNmo1VG9IT2VKZ1NvcWNpMXRFQWhIaFUtZDAzQUJwdjJJSkVXYVRNaWlRdFk0cjkyZVZ1aEtwNHl3SUNvQ1pfSHlQX1J5T1FlMy1aTDVNLWZsa0FnIiwiaXNzIjoiSUpzczZVVVVxQjlrVVZoUldmN3pEb2xGbXltS3Nsdm5zR0tfN1FlUVZzYyIsInNjb3BlIjoicXVvdGU6KiJ9.h3qIw676jUUi2cp0gwm4lx1KEgRydDJ2dTbyCGTsfg8KMHy-rV8KfqFmRvT32Y8rSBhbhbgl9eiBI1zGhTbJDQ", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "too_deep" + } + }, + { + "name": "five_tokens_fifth_malformed", + "note": "the fifth token is not a compact JWS; depth is counted before it would be decoded, so the outcome is too_deep, not malformed", + "token": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6InZ1bzlQNXl0WEZXQ1l0alBVZTZndGhPQ0tjb3ZJd1ZUaExEOEZuWlREQkUifSwidHlwIjoiZm9yYS1kZWxlZ2F0aW9uK2p3dCJ9.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImZvcmFfcGFyZW50IjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJbXAzYXlJNmV5SmpjbllpT2lKRlpESTFOVEU1SWl3aWEzUjVJam9pVDB0UUlpd2llQ0k2SW04d09FaHFRVjlYT1RNNVkySmpkV3hOVWtwTmMyNDNkM0ZVU2tGUU5uWkthV0o2WVRsRWVqbDBVVFFpZlN3aWRIbHdJam9pWm05eVlTMWtaV3hsWjJGMGFXOXVLMnAzZENKOS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVaWGhoYlhCc1pTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaVJuSXRUbU5IYWtaNVNUWmhOazR6VUVKdlJrb3lOM04yUjJWdFdubGtOSFZuTUVkc1ZWOWxUMjFOT0NKOUxDSmxlSEFpT2pFM09EQXpNVGc0TURBc0ltWnZjbUZmY0dGeVpXNTBJam9pWlhsS2FHSkhZMmxQYVVwR1drVlNWRkZUU1hOSmJYQXpZWGxKTm1WNVNtcGpibGxwVDJsS1JscEVTVEZPVkVVMVNXbDNhV0V6VWpWSmFtOXBWREIwVVVscGQybGxRMGsyU1c1c1IwMUdUbXRqVlhneVVqSmFOVk5HV1RCVVJtUnZWRVV3ZEZvd01UUk9NMHBSV2pFNVRWUllhR0ZUTTBKM1UyMVNXazR5TlZOa2JHdHBabE4zYVdSSWJIZEphbTlwV20wNWVWbFRNV3RhVjNoc1dqSkdNR0ZYT1hWTE1uQXpaRU5LT1M1bGVVcG9aRmRSYVU5c2MybGFXR2hxWVVkR2RWb3lWWFZhV0dob1lsaENjMXBUU21STVEwcHFZbTFaYVU5dWMybGhiWFF3U1dwdmFVNVlVa05TUms0elZqTkNNMUpXT1d0aGJVWmFZMWQ0Y2xGdFVsTlVhMW8yVkdwYVNGb3pjRWhUYld4WVlucFNhRnBWYXpSWmExSlhWbE5LT1V4RFNteGxTRUZwVDJwRk0wOUVRWHBOVkdjMFRVUkJjMGx0V25aamJVWm1ZMGRHZVZwWE5UQkphbTlwV2xoc1MyRkhTa2haTW14UVlWVndSMWRyVmxOV1JrWlVVMWhPU21Kc1NURlpNRTVLVG10c2RGZHVXbXBpVlZZd1YydGtWMk14Y0ZoYVIyaHJVako0TWxsdGJEQmpWMUY2VlZkc2JWVlROV3hsVlhCdldrWmtVbUZWT1hOak1teGhWMGRvY1ZsVlpFZGtWbTk1VmxoV1lWZEhhRzlaYkdoRFl6RndWRk50VWsxUk1IQnhXVzB4V21GVk9YVmpNbXhvWWxoUmQxTlhjSFpoVm1SSVZXcE9XbGRGY0VaVWExWXpaREF4VjFGc2NFOWlSVnBvVkZWb1UyRkZNVFpSYmtKcFlUQndUbFZxVG1GV1ZUVlZXa2hDYTFKVVJsWldNVlp6WVRBeGNtRkVXbEpOTUZwVVZXeE9TMDlWZUVSVGJYaHNVMFZHY0ZReWNFWk5NRGxGVVZod1RsWkhZekJVVlZKQ1l6QnNkRmR1V21waVZWcHRXVEJrUjJWV2NGaE9WRUpLWVcwNWNGbHRNRFZOUlhoWVVsaFNXazFxYkRCWk1HUkhZVzFTUkUxWVJtdE5NREZ3VkVWT1MyTkhUWHBVVjJ4UVlWVndNRmRXYUV0amJIQlpWVzEwV2xkR1NtOVVSekZYVGtac1dFMVlaR2xTTVZad1ZFVk9TMlZzYTNsUFdHUmhWVEJyTWxOWE5VZE5WMGw2VlcxNFVHRlhPWEJhYkVWMVpEQTFTR1F6WjNoUFIzUjNVMjA1YVdOWWFFWldWV3h1WW5wU2IweFhjREpUTUZWNVlXdGpNbVZZU25GVFNGa3dZek5XUTFaVVFrMU9iRGxwVFcxU1VHTkhOV2xVYm1oNVlsZDBabFl5TVZsWk1uQkVVMnhOZVdSRlNqRmFWWGh6VVZWa05XSnVWa2hhU0Zac1ZETm9lbEZWUldsTVEwcHdZek5OYVU5cFNsbGFTR1JvWTJ0Uk1GUkVRWGhWUm1zeVZWWnZkMlJIUlhwTlIyeDFVV3Q0U0dSc1VURk9NbXd3VkZaU1dsTlhVWGxUU0hCRVkxWktSa2xwZDJsak1rNTJZMGRWYVU5cFNuaGtWemt3V2xSdmNVbHVNQzVoU1V0NmVqSTBjMDlPTTAxeFZFRnJXVlZ5U0d0SlpVYzFlbXBuYm1adWR5MTZXRlpmU1ZOMVFqSmtOVGxRWDNaellrVnBWVVZxY1RGblozTkZWa1JVWXpSbFZsUkpTR0pmZDJkNVJtTlVRbEk0VlhGQ2R5SXNJbWx6Y3lJNklqVjBRa1JUZDFkd2QwVmZaR3BoV1hGc2EwSmtVazVHZWs0MlIyZDZSMHBwVjI4MFlXVkpPR0pFVmxVaUxDSnpZMjl3WlNJNkluRjFiM1JsT2lvaWZRLktVXzYtaEcwVGYxczdNRmRNRDY5bzk2SE5yVE1kbUsyQTRpLXQ2aktwUUt3QUJzZEs3QmFac2taTWtqeXhPX1F3bFZ3X1lKd0JHYzd2cWxYaXdwY0J3IiwiaXNzIjoiRnItTmNHakZ5STZhNk4zUEJvRkoyN3N2R2VtWnlkNHVnMEdsVV9lT21NOCIsInNjb3BlIjoicXVvdGU6KiJ9.2wGABtn6HWIcMcnv1uWrgeJonLc19j1OrGFJ94qUd1MKvmrLRBRsimTg8-e9rMxjPVYCfkZGVFJJp_l94j8tCg", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "too_deep" + } + }, + { + "name": "jti_present", + "note": "jti is recommended and checked for shape only", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImlzcyI6Im1hcmtldGRhdGEuZXhhbXBsZSIsImp0aSI6Im1kLTIwMjYtMDAwNDgxIiwic2NvcGUiOiJxdW90ZToqIn0.Ml16C75OaEql47jolG1GGwjEZsYfOz0DJneedtnLUhDM1Pbsd1Z_Bb5GvABBbclqXKsAFotAmSVtdZ382Q-wDQ", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": true, + "root_issuer": "marketdata.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "scopes": [ + "quote:*" + ], + "expires_at": "2026-06-01T13:00:00Z", + "depth": 1 + } + }, + { + "name": "jti_empty", + "note": "an empty jti is a bad claim", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImlzcyI6Im1hcmtldGRhdGEuZXhhbXBsZSIsImp0aSI6IiIsInNjb3BlIjoicXVvdGU6KiJ9.P5Pj9twnd2WK94B04miMYtzWhGkNVM7lMQ3cUzj3fUSmcUzKlTkHGH7ws2nRN54zGWmCIV6mU__Fa9U_pBg2Cw", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "bad_claim" + } + }, + { + "name": "typ_missing", + "note": "typ fora-delegation+jwt is required on every token", + "token": "eyJhbGciOiJFZERTQSJ9.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImlzcyI6Im1hcmtldGRhdGEuZXhhbXBsZSIsInNjb3BlIjoicXVvdGU6KiJ9.TEbAb8pH1kOC75CaxiEg20CWmdD4prQm0vYNGE4-nBXOBMuYi2T6X8zhUC51JQJ7xRLwdJaS2U_f_RtOt1xRDw", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "bad_typ" + } + }, + { + "name": "typ_wrong_on_child", + "note": "typ JWT on a child", + "token": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6InlGMFNkcUx2R2Z5SFY0TFdoTE0tZ014N3JQZ19MTXhaS3BwSmRZN25SdlkifSwidHlwIjoiSldUIn0.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImZvcmFfcGFyZW50IjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNkltWnZjbUV0WkdWc1pXZGhkR2x2Yml0cWQzUWlmUS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVaWGhoYlhCc1pTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaVdHUjNZWEpFTkV3d01WQlpObEZhTUhSaE16QnBia0pNUjNaVU5UZHBkRTFVV1Vsa01raDZRM0ZTUlNKOUxDSmxlSEFpT2pFM09EQXpNVGc0TURBc0ltbHpjeUk2SW0xaGNtdGxkR1JoZEdFdVpYaGhiWEJzWlNJc0luTmpiM0JsSWpvaWNYVnZkR1U2S2lKOS5va0dIanBoMHFCZTFvMnZSNmZ4UmEyRkdScnh0Qi1hVHZnTU1yODBZQXliV0hpMjRpOEhvLUZnSzRxWTl6ZGZUV0lrajhNem1jRnhTWG53eFBDTG9BZyIsImlzcyI6Ilhkd2FyRDRMMDFQWTZRWjB0YTMwaW5CTEd2VDU3aXRNVFlJZDJIekNxUkUiLCJzY29wZSI6InF1b3RlOioifQ.9UhS2CIqbQjQ148WOW56ztDM4bXTjvStKrDIuhLn5KhLt062ZcngqFGczdy3iLZU4Md6HroCiI-t1jmXppSHAA", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "bad_typ" + } + }, + { + "name": "root_typ_wrong_under_child", + "note": "typ JWT on the root under a valid child: every token's header is checked", + "token": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6InlGMFNkcUx2R2Z5SFY0TFdoTE0tZ014N3JQZ19MTXhaS3BwSmRZN25SdlkifSwidHlwIjoiZm9yYS1kZWxlZ2F0aW9uK2p3dCJ9.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImZvcmFfcGFyZW50IjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNklrcFhWQ0o5LmV5SmhkV1FpT2xzaVpYaGphR0Z1WjJVdVpYaGhiWEJzWlNKZExDSmpibVlpT25zaWFtdDBJam9pV0dSM1lYSkVORXd3TVZCWk5sRmFNSFJoTXpCcGJrSk1SM1pVTlRkcGRFMVVXVWxrTWtoNlEzRlNSU0o5TENKbGVIQWlPakUzT0RBek1UZzRNREFzSW1semN5STZJbTFoY210bGRHUmhkR0V1WlhoaGJYQnNaU0lzSW5OamIzQmxJam9pY1hWdmRHVTZLaUo5Ll9WQklldTFOQVBsMUlLVnlSNkRhczNMbmdXcG8zWDNnSUJ4Y00yUGY3T2xGR3l2SWU2N0dLeFQ2enVNZGJZU1ZCdnNHUFY3WTNEem9aRm8tNWlFd0F3IiwiaXNzIjoiWGR3YXJENEwwMVBZNlFaMHRhMzBpbkJMR3ZUNTdpdE1UWUlkMkh6Q3FSRSIsInNjb3BlIjoicXVvdGU6KiJ9.Wi01--_tsrMPHd_MjTrRj9TGgULmjwbiKLirAMjF7y4_JwltXr-Le7UCDRdFAb_h6wOzqzBzxlOKl9L-dvsnBQ", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "bad_typ" + } + }, + { + "name": "middle_typ_wrong", + "note": "typ JWT on the middle token of three", + "token": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im8wOEhqQV9XOTM5Y2JjdWxNUkpNc243d3FUSkFQNnZKaWJ6YTlEejl0UTQifSwidHlwIjoiZm9yYS1kZWxlZ2F0aW9uK2p3dCJ9.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImZvcmFfcGFyZW50IjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJbXAzYXlJNmV5SmpjbllpT2lKRlpESTFOVEU1SWl3aWEzUjVJam9pVDB0UUlpd2llQ0k2SW5sR01GTmtjVXgyUjJaNVNGWTBURmRvVEUwdFowMTROM0pRWjE5TVRYaGFTM0J3U21SWk4yNVNkbGtpZlN3aWRIbHdJam9pU2xkVUluMC5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVaWGhoYlhCc1pTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaU5YUkNSRk4zVjNCM1JWOWthbUZaY1d4clFtUlNUa1o2VGpaSFozcEhTbWxYYnpSaFpVazRZa1JXVlNKOUxDSmxlSEFpT2pFM09EQXpNVGc0TURBc0ltWnZjbUZmY0dGeVpXNTBJam9pWlhsS2FHSkhZMmxQYVVwR1drVlNWRkZUU1hOSmJsSTFZME5KTmtsdFduWmpiVVYwV2tkV2MxcFhaR2hrUjJ4MlltbDBjV1F6VVdsbVVTNWxlVXBvWkZkUmFVOXNjMmxhV0docVlVZEdkVm95VlhWYVdHaG9ZbGhDYzFwVFNtUk1RMHBxWW0xWmFVOXVjMmxoYlhRd1NXcHZhVmRIVWpOWldFcEZUa1YzZDAxV1FscE9iRVpoVFVoU2FFMTZRbkJpYTBwTlVqTmFWVTVVWkhCa1JURlZWMVZzYTAxcmFEWlJNMFpUVWxOS09VeERTbXhsU0VGcFQycEZNMDlFUVhwTlZHYzBUVVJCYzBsdGJIcGplVWsyU1cweGFHTnRkR3hrUjFKb1pFZEZkVnBZYUdoaVdFSnpXbE5KYzBsdVRtcGlNMEpzU1dwdmFXTllWblprUjFVMlMybEtPUzV2YTBkSWFuQm9NSEZDWlRGdk1uWlNObVo0VW1FeVJrZFNjbmgwUWkxaFZIWm5UVTF5T0RCWlFYbGlWMGhwTWpScE9FaHZMVVpuU3pSeFdUbDZaR1pVVjBscmFqaE5lbTFqUm5oVFdHNTNlRkJEVEc5Qlp5SXNJbWx6Y3lJNklsaGtkMkZ5UkRSTU1ERlFXVFpSV2pCMFlUTXdhVzVDVEVkMlZEVTNhWFJOVkZsSlpESklla054VWtVaUxDSnpZMjl3WlNJNkluRjFiM1JsT2lvaWZRLlBONlBBRVNKSkhFVk44bl9sazI1UUVTY2RTN3RzdXFwTTJKazEzWXBmQ2hpU0NLUUxsTExuNHg3cXk2bWZpWnRTOVFlLTNVTUtLNm9mbVA3OHRGY0NnIiwiaXNzIjoiNXRCRFN3V3B3RV9kamFZcWxrQmRSTkZ6TjZHZ3pHSmlXbzRhZUk4YkRWVSIsInNjb3BlIjoicXVvdGU6KiJ9.KK-VgzKx8InnZNg85gOW_JoHbA11SceEwgRGhnnYkPYXWppkMfpYJ2pggxXETJxgUMXlm6ARmsolQwoD5SqjBg", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "bad_typ" + } + }, + { + "name": "alg_es256", + "note": "only EdDSA and Ed25519 are accepted", + "token": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImlzcyI6Im1hcmtldGRhdGEuZXhhbXBsZSIsInNjb3BlIjoicXVvdGU6KiJ9.EHjBUJ5gZtO0Aob9mX6_KL6aK0LyRlkztL9s92WoZjnvGieuwRrwJllrh4fgEZ7lBfpb3b3RTbX3Wtw4vMX_BQ", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "bad_alg" + } + }, + { + "name": "crit_present", + "note": "the profile defines no critical header parameters", + "token": "eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il0sInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImlzcyI6Im1hcmtldGRhdGEuZXhhbXBsZSIsInNjb3BlIjoicXVvdGU6KiJ9.6Yu5Hq0nXe6ykU1qNN86BA0qe0nxdpkeV81g8BeVMmCU_-AU60Kys6ALeZrpzYHj6I9Yop0LAv1WST_tylBzBg", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "crit_present" + } + }, + { + "name": "iss_missing", + "note": "", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsInNjb3BlIjoicXVvdGU6KiJ9.dzbdn8MfsFjrKDyov74PGvgoMUYqp-jnnUtbFQMifZGeOI4c8c-CX8iNvUZrZFoF15fNzvzbOX4YmCmBtIJxBQ", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "missing_claim" + } + }, + { + "name": "aud_missing", + "note": "", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImlzcyI6Im1hcmtldGRhdGEuZXhhbXBsZSIsInNjb3BlIjoicXVvdGU6KiJ9.5MFIhNTSm0VCiH0X8a_mO3XsbIdTd35GIjQINUmS6D-mBs23bFBUJKxD0203dZKuQ537FCWYfuYyOP0p50VQBQ", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "missing_claim" + } + }, + { + "name": "cnf_missing", + "note": "", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJleHAiOjE3ODAzMTg4MDAsImlzcyI6Im1hcmtldGRhdGEuZXhhbXBsZSIsInNjb3BlIjoicXVvdGU6KiJ9.ZhxbceL78LTut1AWEtP4u0_IAJ_hfhHqkEL7f6CyA6f6KiVE_QlZz425-3WkF1J5HoWRpyH0cc9LcjFnzFqoAg", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "missing_claim" + } + }, + { + "name": "exp_missing", + "note": "", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJpc3MiOiJtYXJrZXRkYXRhLmV4YW1wbGUiLCJzY29wZSI6InF1b3RlOioifQ.ujo5k2I1M8tvma-QmO6N2k3e37eyPRU8QV8QIwv1zwLIQ-N9WzWtzXjRP84mNDICVKYSCHWlNzagOQQucG-vBw", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "missing_claim" + } + }, + { + "name": "exp_not_a_number", + "note": "exp as a string", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOiIxNzgxNzg3MzQ1IiwiaXNzIjoibWFya2V0ZGF0YS5leGFtcGxlIiwic2NvcGUiOiJxdW90ZToqIn0.oRgYM86fdxIBMxEuiYCVhr0FYr0_iP-PLUYb1UmY0gADCJEk8_th2ZIUbIhLuxXN-2iV7HUPL_Sqho5hNZfADA", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "bad_claim" + } + }, + { + "name": "exp_out_of_range", + "note": "exp 1e400 is not a finite number", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjFlNDAwLCJpc3MiOiJtYXJrZXRkYXRhLmV4YW1wbGUiLCJzY29wZSI6InF1b3RlOioifQ.msD5lDXQb3GpI1x0H3iAiRDSKEbFea5bAxgH9QhLELZcTeVG36_1RZPyn0IcJPub2M-RsF6oThGucN_BLPrvBg", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "bad_claim" + } + }, + { + "name": "exp_inside_leeway", + "note": "59 seconds past exp is accepted; the returned expiry is still exp", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTUxNDEsImlzcyI6Im1hcmtldGRhdGEuZXhhbXBsZSIsInNjb3BlIjoicXVvdGU6KiJ9.BAp7LImazLTigcM56VcLAZbT_XLmFTZb2NaI2z3Njeee4FT7bPEAnHrOpPn7I_pPlDHgyTwESy1A6RHv-B5qAA", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": true, + "root_issuer": "marketdata.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "scopes": [ + "quote:*" + ], + "expires_at": "2026-06-01T11:59:01Z", + "depth": 1 + } + }, + { + "name": "exp_at_leeway", + "note": "60 seconds past exp is expired", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTUxNDAsImlzcyI6Im1hcmtldGRhdGEuZXhhbXBsZSIsInNjb3BlIjoicXVvdGU6KiJ9.hPoMB8JK5bKVMMU-MpMwm0w4lF0Hlkz3KndSC6rbQ63i1yH4FrWCd4EhHRAOHsSCcTTXOigHBh7MVY7GUbjsAw", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "expired", + "token_index": 1 + } + }, + { + "name": "nbf_inside_leeway", + "note": "nbf 60 seconds ahead is accepted", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImlzcyI6Im1hcmtldGRhdGEuZXhhbXBsZSIsIm5iZiI6MTc4MDMxNTI2MCwic2NvcGUiOiJxdW90ZToqIn0.Qd16JmDoO9GsUe91hcRrpt16Ys6lQanPR27b585v39BvF8p-gWGt87dA2hhccSTNTf1_4LBalvr5bUJYiRsrDQ", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": true, + "root_issuer": "marketdata.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "scopes": [ + "quote:*" + ], + "expires_at": "2026-06-01T13:00:00Z", + "depth": 1 + } + }, + { + "name": "nbf_beyond_leeway", + "note": "nbf 61 seconds ahead is not yet valid", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImlzcyI6Im1hcmtldGRhdGEuZXhhbXBsZSIsIm5iZiI6MTc4MDMxNTI2MSwic2NvcGUiOiJxdW90ZToqIn0.nTNmbc29Ibcj8DrlyRQ7Z30OKb9yAPv2fkmNBiM98jZAzih_g_vO-knfbzYOO3hl3t3BvPGUsAw2plz1Pm-RDQ", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "not_yet_valid", + "token_index": 1 + } + }, + { + "name": "root_nbf_beyond_leeway", + "note": "nbf on the root only, under a valid child: time is checked on every token", + "token": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6InlGMFNkcUx2R2Z5SFY0TFdoTE0tZ014N3JQZ19MTXhaS3BwSmRZN25SdlkifSwidHlwIjoiZm9yYS1kZWxlZ2F0aW9uK2p3dCJ9.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImZvcmFfcGFyZW50IjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNkltWnZjbUV0WkdWc1pXZGhkR2x2Yml0cWQzUWlmUS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVaWGhoYlhCc1pTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaVdHUjNZWEpFTkV3d01WQlpObEZhTUhSaE16QnBia0pNUjNaVU5UZHBkRTFVV1Vsa01raDZRM0ZTUlNKOUxDSmxlSEFpT2pFM09EQXpNVGc0TURBc0ltbHpjeUk2SW0xaGNtdGxkR1JoZEdFdVpYaGhiWEJzWlNJc0ltNWlaaUk2TVRjNE1ETXhOVEkyTVN3aWMyTnZjR1VpT2lKeGRXOTBaVG9xSW4wLnJBSXdMeGQtVG15WG9kN1RoQVZzUmtPdEVQNk5TNVQ3emJyWHVuTURHNXZVYzJqbEVRY2dvWE85S1N6LUFhZEdQMWpNZjMwZnJWUGJPUFNPVmtSQ0RRIiwiaXNzIjoiWGR3YXJENEwwMVBZNlFaMHRhMzBpbkJMR3ZUNTdpdE1UWUlkMkh6Q3FSRSIsInNjb3BlIjoicXVvdGU6KiJ9.nwPJq3y19H9NRSI-zviovv3b2v2eokc_HkDAJ_gsUw2aIjcuuL-STxk7k5bQuXvWxLiT2-o1CKHTXS30W1MZBA", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "not_yet_valid", + "token_index": 1 + } + }, + { + "name": "middle_nbf_beyond_leeway", + "note": "nbf on the middle token of three", + "token": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im8wOEhqQV9XOTM5Y2JjdWxNUkpNc243d3FUSkFQNnZKaWJ6YTlEejl0UTQifSwidHlwIjoiZm9yYS1kZWxlZ2F0aW9uK2p3dCJ9.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImZvcmFfcGFyZW50IjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJbXAzYXlJNmV5SmpjbllpT2lKRlpESTFOVEU1SWl3aWEzUjVJam9pVDB0UUlpd2llQ0k2SW5sR01GTmtjVXgyUjJaNVNGWTBURmRvVEUwdFowMTROM0pRWjE5TVRYaGFTM0J3U21SWk4yNVNkbGtpZlN3aWRIbHdJam9pWm05eVlTMWtaV3hsWjJGMGFXOXVLMnAzZENKOS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVaWGhoYlhCc1pTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaU5YUkNSRk4zVjNCM1JWOWthbUZaY1d4clFtUlNUa1o2VGpaSFozcEhTbWxYYnpSaFpVazRZa1JXVlNKOUxDSmxlSEFpT2pFM09EQXpNVGc0TURBc0ltWnZjbUZmY0dGeVpXNTBJam9pWlhsS2FHSkhZMmxQYVVwR1drVlNWRkZUU1hOSmJsSTFZME5KTmtsdFduWmpiVVYwV2tkV2MxcFhaR2hrUjJ4MlltbDBjV1F6VVdsbVVTNWxlVXBvWkZkUmFVOXNjMmxhV0docVlVZEdkVm95VlhWYVdHaG9ZbGhDYzFwVFNtUk1RMHBxWW0xWmFVOXVjMmxoYlhRd1NXcHZhVmRIVWpOWldFcEZUa1YzZDAxV1FscE9iRVpoVFVoU2FFMTZRbkJpYTBwTlVqTmFWVTVVWkhCa1JURlZWMVZzYTAxcmFEWlJNMFpUVWxOS09VeERTbXhsU0VGcFQycEZNMDlFUVhwTlZHYzBUVVJCYzBsdGJIcGplVWsyU1cweGFHTnRkR3hrUjFKb1pFZEZkVnBZYUdoaVdFSnpXbE5KYzBsdVRtcGlNMEpzU1dwdmFXTllWblprUjFVMlMybEtPUzV2YTBkSWFuQm9NSEZDWlRGdk1uWlNObVo0VW1FeVJrZFNjbmgwUWkxaFZIWm5UVTF5T0RCWlFYbGlWMGhwTWpScE9FaHZMVVpuU3pSeFdUbDZaR1pVVjBscmFqaE5lbTFqUm5oVFdHNTNlRkJEVEc5Qlp5SXNJbWx6Y3lJNklsaGtkMkZ5UkRSTU1ERlFXVFpSV2pCMFlUTXdhVzVDVEVkMlZEVTNhWFJOVkZsSlpESklla054VWtVaUxDSnVZbVlpT2pFM09EQXpNVFV5TmpFc0luTmpiM0JsSWpvaWNYVnZkR1U2S2lKOS5QdTY1clhaYzhiUm84dFpLRHB2MV9jOU80aTJVT1pKNjBPT29SOVA2MUlUY0w1UnRsY3RRTF94LWtqLVpOY1ZITHA1N3hrLW5lSXhiT2ptZ21lbzRDQSIsImlzcyI6IjV0QkRTd1dwd0VfZGphWXFsa0JkUk5Gek42R2d6R0ppV280YWVJOGJEVlUiLCJzY29wZSI6InF1b3RlOioifQ.Mjdlr9T-X0W-ChZrcG8zZcTnulAxKxw6UhtxSk52en2qfS-ujb_xtKO1vSehDDAEzN_3zhEgqwjw00U2Cp42Dw", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "not_yet_valid", + "token_index": 2 + } + }, + { + "name": "child_expires_first", + "note": "the effective expiry is the earliest exp in the chain", + "token": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6InlGMFNkcUx2R2Z5SFY0TFdoTE0tZ014N3JQZ19MTXhaS3BwSmRZN25SdlkifSwidHlwIjoiZm9yYS1kZWxlZ2F0aW9uK2p3dCJ9.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTgyMDAsImZvcmFfcGFyZW50IjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNkltWnZjbUV0WkdWc1pXZGhkR2x2Yml0cWQzUWlmUS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVaWGhoYlhCc1pTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaVdHUjNZWEpFTkV3d01WQlpObEZhTUhSaE16QnBia0pNUjNaVU5UZHBkRTFVV1Vsa01raDZRM0ZTUlNKOUxDSmxlSEFpT2pFM09EQXpNVGc0TURBc0ltbHpjeUk2SW0xaGNtdGxkR1JoZEdFdVpYaGhiWEJzWlNJc0luTmpiM0JsSWpvaWNYVnZkR1U2S2lKOS5va0dIanBoMHFCZTFvMnZSNmZ4UmEyRkdScnh0Qi1hVHZnTU1yODBZQXliV0hpMjRpOEhvLUZnSzRxWTl6ZGZUV0lrajhNem1jRnhTWG53eFBDTG9BZyIsImlzcyI6Ilhkd2FyRDRMMDFQWTZRWjB0YTMwaW5CTEd2VDU3aXRNVFlJZDJIekNxUkUiLCJzY29wZSI6InF1b3RlOioifQ.mT-82tK9NT4wC9BIH9o9NM5E3FmL60EsJX6t3c50ACr5VfYOajpKer3cey_mWb95fACDqF_SknJU9x7mXuVUCw", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": true, + "root_issuer": "marketdata.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "scopes": [ + "quote:*" + ], + "expires_at": "2026-06-01T12:50:00Z", + "depth": 2 + } + }, + { + "name": "audience_other_recipient", + "note": "aud names another Exchange", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOiJldS5leGNoYW5nZS5leGFtcGxlIiwiY25mIjp7ImprdCI6IldpLXZOVHZPODRwb2RKcEl2WEtVbEdORFV1aEtKS1M0elZ6aG5SdlQzWTQifSwiZXhwIjoxNzgwMzE4ODAwLCJpc3MiOiJtYXJrZXRkYXRhLmV4YW1wbGUiLCJzY29wZSI6InF1b3RlOioifQ.JU2zXpyBseThF6GJZ04zfbw-ZQe1PfIBPaENl4NhObgzvlSnDH0zYeMrImaMoNSTiBIrcxouHGyOz5osxHQ0Dg", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "audience_mismatch", + "token_index": 1 + } + }, + { + "name": "audience_case_and_443_fold", + "note": "Exchange.Example:443 names exchange.example", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOiJFeGNoYW5nZS5FeGFtcGxlOjQ0MyIsImNuZiI6eyJqa3QiOiJXaS12TlR2Tzg0cG9kSnBJdlhLVWxHTkRVdWhLSktTNHpWemhuUnZUM1k0In0sImV4cCI6MTc4MDMxODgwMCwiaXNzIjoibWFya2V0ZGF0YS5leGFtcGxlIiwic2NvcGUiOiJxdW90ZToqIn0.kFjBvWf_l3qbASHQ8g9KuSFrpk5U9lAk5xwURgUr-ywUUlkFAWikrpwqY-mPi3K4QGvjL_QzkwSn-DrMisncAQ", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": true, + "root_issuer": "marketdata.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "scopes": [ + "quote:*" + ], + "expires_at": "2026-06-01T13:00:00Z", + "depth": 1 + } + }, + { + "name": "audience_not_bare_host", + "note": "a URL is not a recipient identity", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOiJodHRwczovL2V4Y2hhbmdlLmV4YW1wbGUiLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImlzcyI6Im1hcmtldGRhdGEuZXhhbXBsZSIsInNjb3BlIjoicXVvdGU6KiJ9.-VwnDFP9cWxC-FFsYhv2wBPR4WMlTxJduXAasmiwf5VtXolHUmXAMz46S85x0oDYAmDFxO-rFQWfu1wTeD79Dg", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "bad_claim" + } + }, + { + "name": "child_widens_audience", + "note": "a child adds a recipient its parent did not list", + "token": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6InlGMFNkcUx2R2Z5SFY0TFdoTE0tZ014N3JQZ19MTXhaS3BwSmRZN25SdlkifSwidHlwIjoiZm9yYS1kZWxlZ2F0aW9uK2p3dCJ9.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSIsImV1LmV4Y2hhbmdlLmV4YW1wbGUiXSwiY25mIjp7ImprdCI6IldpLXZOVHZPODRwb2RKcEl2WEtVbEdORFV1aEtKS1M0elZ6aG5SdlQzWTQifSwiZXhwIjoxNzgwMzE4ODAwLCJmb3JhX3BhcmVudCI6ImV5SmhiR2NpT2lKRlpFUlRRU0lzSW5SNWNDSTZJbVp2Y21FdFpHVnNaV2RoZEdsdmJpdHFkM1FpZlEuZXlKaGRXUWlPbHNpWlhoamFHRnVaMlV1WlhoaGJYQnNaU0pkTENKamJtWWlPbnNpYW10MElqb2lXR1IzWVhKRU5Fd3dNVkJaTmxGYU1IUmhNekJwYmtKTVIzWlVOVGRwZEUxVVdVbGtNa2g2UTNGU1JTSjlMQ0psZUhBaU9qRTNPREF6TVRnNE1EQXNJbWx6Y3lJNkltMWhjbXRsZEdSaGRHRXVaWGhoYlhCc1pTSXNJbk5qYjNCbElqb2ljWFZ2ZEdVNktpSjkub2tHSGpwaDBxQmUxbzJ2UjZmeFJhMkZHUnJ4dEItYVR2Z01NcjgwWUF5YldIaTI0aThIby1GZ0s0cVk5emRmVFdJa2o4TXptY0Z4U1hud3hQQ0xvQWciLCJpc3MiOiJYZHdhckQ0TDAxUFk2UVowdGEzMGluQkxHdlQ1N2l0TVRZSWQySHpDcVJFIiwic2NvcGUiOiJxdW90ZToqIn0.FYKGJEkqRumeY8ZVMah3oxjGCJfEMt06tJ80ZeGwckdhb8WZWytklaskqIUTFbYhV1mS8ynEwFt1KXRRdCr0Dg", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "audience_widened", + "token_index": 2 + } + }, + { + "name": "child_narrows_audience", + "note": "a child drops one of the parent's recipients", + "token": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6InlGMFNkcUx2R2Z5SFY0TFdoTE0tZ014N3JQZ19MTXhaS3BwSmRZN25SdlkifSwidHlwIjoiZm9yYS1kZWxlZ2F0aW9uK2p3dCJ9.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImZvcmFfcGFyZW50IjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNkltWnZjbUV0WkdWc1pXZGhkR2x2Yml0cWQzUWlmUS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVaWGhoYlhCc1pTSXNJbVYxTG1WNFkyaGhibWRsTG1WNFlXMXdiR1VpWFN3aVkyNW1JanA3SW1wcmRDSTZJbGhrZDJGeVJEUk1NREZRV1RaUldqQjBZVE13YVc1Q1RFZDJWRFUzYVhSTlZGbEpaREpJZWtOeFVrVWlmU3dpWlhod0lqb3hOemd3TXpFNE9EQXdMQ0pwYzNNaU9pSnRZWEpyWlhSa1lYUmhMbVY0WVcxd2JHVWlMQ0p6WTI5d1pTSTZJbkYxYjNSbE9pb2lmUS41YkYxUXo2eXhkcm9rWGI3Uk1tUVpUak5IQkIycGdZOVgxR0VhYkdBN3VNckxidHVwcUhVdW1UWUF2ODM2MlRZWXZBaTRkRVdZcEdQZ2x4d25kazRDUSIsImlzcyI6Ilhkd2FyRDRMMDFQWTZRWjB0YTMwaW5CTEd2VDU3aXRNVFlJZDJIekNxUkUiLCJzY29wZSI6InF1b3RlOioifQ.s-Wi8QlMxo1qF-wcZrGnNC3fb7jSnhk9Lyr_dvgdl1tcWXjR0ReSi9zO51YWc4ocQPNT7-Wau_fGNmjjTgCCBQ", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": true, + "root_issuer": "marketdata.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "scopes": [ + "quote:*" + ], + "expires_at": "2026-06-01T13:00:00Z", + "depth": 2 + } + }, + { + "name": "untrusted_root", + "note": "the root is signed by a key that is not configured", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImlzcyI6Im1hcmtldGRhdGEuZXhhbXBsZSIsInNjb3BlIjoicXVvdGU6KiJ9.VQAPTDsuC-wAN9UVlUbl6lZwiLyiUnITeE2fAVXQ2sdGaxEn6rMlURIkuJYCAIGNVK1IQycyHAOmCpzy7hHHBw", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "bad_signature", + "token_index": 1 + } + }, + { + "name": "issuer_two_keys_old_signed", + "note": "two keys listed under one identity while a key is being replaced; the root signed with the old key verifies", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImlzcyI6Im1hcmtldGRhdGEuZXhhbXBsZSIsInNjb3BlIjoicXVvdGU6KiJ9.pr2ux5pWGLBl8t5SDdj4EyOUBkaH_WyRqR3DR2xawwuTAK64Ky13IdrilNJNrhNlZlHHJpm58p15dU0uCxHXAA", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + }, + { + "identity": "marketdata.example", + "key_x": "Gbry6M2SGFcpzBJCnfNhyd2WvkuQxEN_hRWojjOvQXE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": true, + "root_issuer": "marketdata.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "scopes": [ + "quote:*" + ], + "expires_at": "2026-06-01T13:00:00Z", + "depth": 1 + } + }, + { + "name": "issuer_two_keys_new_signed", + "note": "two keys listed under one identity; the root signed with the new key verifies", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImlzcyI6Im1hcmtldGRhdGEuZXhhbXBsZSIsInNjb3BlIjoicXVvdGU6KiJ9.2IaWHk2tAoGcQZchHb7JheETHRLiJ8c62M6zC54pO8cly_pihQSWm6xy5fRHXaaibNTkTVDx6kkePRWL0TZWDg", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + }, + { + "identity": "marketdata.example", + "key_x": "Gbry6M2SGFcpzBJCnfNhyd2WvkuQxEN_hRWojjOvQXE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": true, + "root_issuer": "marketdata.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "scopes": [ + "quote:*" + ], + "expires_at": "2026-06-01T13:00:00Z", + "depth": 1 + } + }, + { + "name": "issuer_key_withdrawn", + "note": "only the new key is listed; a root signed with the withdrawn key is refused", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImlzcyI6Im1hcmtldGRhdGEuZXhhbXBsZSIsInNjb3BlIjoicXVvdGU6KiJ9.pr2ux5pWGLBl8t5SDdj4EyOUBkaH_WyRqR3DR2xawwuTAK64Ky13IdrilNJNrhNlZlHHJpm58p15dU0uCxHXAA", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "Gbry6M2SGFcpzBJCnfNhyd2WvkuQxEN_hRWojjOvQXE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "bad_signature", + "token_index": 1 + } + }, + { + "name": "root_iss_not_configured", + "note": "root iss names an identity with no configured key", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImlzcyI6Im90aGVyLmV4YW1wbGUiLCJzY29wZSI6InF1b3RlOioifQ.Ak978yA8msDw_oKqcEpgN9KCeVKBxOpIPKk5hSg8sqKFf6Hi0WzO6hC3K70D36vroSNNnpjuuucJaxJcXS7CCg", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "untrusted_issuer", + "token_index": 1 + } + }, + { + "name": "root_signature_tampered", + "note": "the root payload was changed after signing", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImlzcyI6Im1hcmtldGRhdGEuZXhhbXBsZSIsInNjb3BlIjoicXVvdGY6KiJ9.pr2ux5pWGLBl8t5SDdj4EyOUBkaH_WyRqR3DR2xawwuTAK64Ky13IdrilNJNrhNlZlHHJpm58p15dU0uCxHXAA", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "bad_signature", + "token_index": 1 + } + }, + { + "name": "child_signature_tampered", + "note": "the child payload was changed after signing", + "token": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6InlGMFNkcUx2R2Z5SFY0TFdoTE0tZ014N3JQZ19MTXhaS3BwSmRZN25SdlkifSwidHlwIjoiZm9yYS1kZWxlZ2F0aW9uK2p3dCJ9.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImZvcmFfcGFyZW50IjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNkltWnZjbUV0WkdWc1pXZGhkR2x2Yml0cWQzUWlmUS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVaWGhoYlhCc1pTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaVdHUjNZWEpFTkV3d01WQlpObEZhTUhSaE16QnBia0pNUjNaVU5UZHBkRTFVV1Vsa01raDZRM0ZTUlNKOUxDSmxlSEFpT2pFM09EQXpNVGc0TURBc0ltbHpjeUk2SW0xaGNtdGxkR1JoZEdFdVpYaGhiWEJzWlNJc0luTmpiM0JsSWpvaWNYVnZkR1U2S2lKOS5va0dIanBoMHFCZTFvMnZSNmZ4UmEyRkdScnh0Qi1hVHZnTU1yODBZQXliV0hpMjRpOEhvLUZnSzRxWTl6ZGZUV0lrajhNem1jRnhTWG53eFBDTG9BZyIsImlzcyI6Ilhkd2FyRDRMMDFQWTZRWjB0YTMwaW5CTEd2VDU3aXRNVFlJZDJIekNxUkUiLCJzY29wZSI6InF1b3RmOioifQ.yLE179xRbLjULSQt9yL1ZNrfUcWA7qUElc3Nj46beZlADSRmHuJCot9ygBFIgwYcfei25Mx476Py8KMeUPdJCw", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "bad_signature", + "token_index": 2 + } + }, + { + "name": "child_iss_wrong", + "note": "signature and jwk linkage are valid but iss is not the signing key's thumbprint", + "token": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6InlGMFNkcUx2R2Z5SFY0TFdoTE0tZ014N3JQZ19MTXhaS3BwSmRZN25SdlkifSwidHlwIjoiZm9yYS1kZWxlZ2F0aW9uK2p3dCJ9.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImZvcmFfcGFyZW50IjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNkltWnZjbUV0WkdWc1pXZGhkR2x2Yml0cWQzUWlmUS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVaWGhoYlhCc1pTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaVdHUjNZWEpFTkV3d01WQlpObEZhTUhSaE16QnBia0pNUjNaVU5UZHBkRTFVV1Vsa01raDZRM0ZTUlNKOUxDSmxlSEFpT2pFM09EQXpNVGc0TURBc0ltbHpjeUk2SW0xaGNtdGxkR1JoZEdFdVpYaGhiWEJzWlNJc0luTmpiM0JsSWpvaWNYVnZkR1U2S2lKOS5va0dIanBoMHFCZTFvMnZSNmZ4UmEyRkdScnh0Qi1hVHZnTU1yODBZQXliV0hpMjRpOEhvLUZnSzRxWTl6ZGZUV0lrajhNem1jRnhTWG53eFBDTG9BZyIsImlzcyI6ImFjbWUiLCJzY29wZSI6InF1b3RlOioifQ.xWK3v3r9CjI6svpuZFSVGe4BtV0w3szCRNDGm4Y6XGBK-E45-jJr8Zlm7kUdzQGVPx7Hirfbt1tV3LNl35rvAA", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "child_issuer_mismatch", + "token_index": 2 + } + }, + { + "name": "child_signed_by_other_key", + "note": "jwk and iss name the principal, the signature was made with another key", + "token": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6InlGMFNkcUx2R2Z5SFY0TFdoTE0tZ014N3JQZ19MTXhaS3BwSmRZN25SdlkifSwidHlwIjoiZm9yYS1kZWxlZ2F0aW9uK2p3dCJ9.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImZvcmFfcGFyZW50IjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNkltWnZjbUV0WkdWc1pXZGhkR2x2Yml0cWQzUWlmUS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVaWGhoYlhCc1pTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaVdHUjNZWEpFTkV3d01WQlpObEZhTUhSaE16QnBia0pNUjNaVU5UZHBkRTFVV1Vsa01raDZRM0ZTUlNKOUxDSmxlSEFpT2pFM09EQXpNVGc0TURBc0ltbHpjeUk2SW0xaGNtdGxkR1JoZEdFdVpYaGhiWEJzWlNJc0luTmpiM0JsSWpvaWNYVnZkR1U2S2lKOS5va0dIanBoMHFCZTFvMnZSNmZ4UmEyRkdScnh0Qi1hVHZnTU1yODBZQXliV0hpMjRpOEhvLUZnSzRxWTl6ZGZUV0lrajhNem1jRnhTWG53eFBDTG9BZyIsImlzcyI6Ilhkd2FyRDRMMDFQWTZRWjB0YTMwaW5CTEd2VDU3aXRNVFlJZDJIekNxUkUiLCJzY29wZSI6InF1b3RlOioifQ.nEn9SS-sx_eOy4PiGsJzjfrH2VORMXDh8FNk8B98tXYcytuKKZWcDZ2lNJ6-uf-vHhDseBIwY5BDqdazdcrcBQ", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "bad_signature", + "token_index": 2 + } + }, + { + "name": "parent_substituted", + "note": "a child re-attached to a different valid root", + "token": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6InlGMFNkcUx2R2Z5SFY0TFdoTE0tZ014N3JQZ19MTXhaS3BwSmRZN25SdlkifSwidHlwIjoiZm9yYS1kZWxlZ2F0aW9uK2p3dCJ9.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImZvcmFfcGFyZW50IjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNkltWnZjbUV0WkdWc1pXZGhkR2x2Yml0cWQzUWlmUS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVaWGhoYlhCc1pTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaVNVcHpjelpWVlZWeFFqbHJWVlpvVWxkbU4zcEViMnhHYlhsdFMzTnNkbTV6UjB0Zk4xRmxVVlp6WXlKOUxDSmxlSEFpT2pFM09EQXpNVGc0TURBc0ltbHpjeUk2SW0xaGNtdGxkR1JoZEdFdVpYaGhiWEJzWlNJc0luTmpiM0JsSWpvaWNYVnZkR1U2S2lKOS5CZER1REU3UVlibDRQY1Z1NFo0cGNublQ5WmhQWGRndW4wN0tZaHZUYm4yQ1JCMUlRQW40Y3YzSmhRTjY3bmNvalZtTWpiMjNsUmhUc1BNS1FPTC1CdyIsImlzcyI6Ilhkd2FyRDRMMDFQWTZRWjB0YTMwaW5CTEd2VDU3aXRNVFlJZDJIekNxUkUiLCJzY29wZSI6InF1b3RlOioifQ.sEynKg8qCP3G4GyPa5p4mWQkrXOvFrSPOPOESgO5n3ZRiB489lkBKe2Q3aXEgIlBgpGHmOoAysZbjTC3AMkEBQ", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "jwk_mismatch", + "token_index": 2 + } + }, + { + "name": "child_jwk_not_parents_cnf", + "note": "the child's jwk is not the key the parent's cnf.jkt names", + "token": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6ImV4NmlHdzZzVUE0cXB5Y2hQajBEZFRzTFVBTzRfYzk1Sl90bl9kYkREeDQifSwidHlwIjoiZm9yYS1kZWxlZ2F0aW9uK2p3dCJ9.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImZvcmFfcGFyZW50IjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNkltWnZjbUV0WkdWc1pXZGhkR2x2Yml0cWQzUWlmUS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVaWGhoYlhCc1pTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaVdHUjNZWEpFTkV3d01WQlpObEZhTUhSaE16QnBia0pNUjNaVU5UZHBkRTFVV1Vsa01raDZRM0ZTUlNKOUxDSmxlSEFpT2pFM09EQXpNVGc0TURBc0ltbHpjeUk2SW0xaGNtdGxkR1JoZEdFdVpYaGhiWEJzWlNJc0luTmpiM0JsSWpvaWNYVnZkR1U2S2lKOS5va0dIanBoMHFCZTFvMnZSNmZ4UmEyRkdScnh0Qi1hVHZnTU1yODBZQXliV0hpMjRpOEhvLUZnSzRxWTl6ZGZUV0lrajhNem1jRnhTWG53eFBDTG9BZyIsImlzcyI6Ilhkd2FyRDRMMDFQWTZRWjB0YTMwaW5CTEd2VDU3aXRNVFlJZDJIekNxUkUiLCJzY29wZSI6InF1b3RlOioifQ.oacVCov8B95n92dmBwfWe1GhqB1bY40fUMUAdSquo18jdYzVm2Nno5t-J1JVJWVGdPUW8HeAtXQAaEUi-mrQDQ", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "jwk_mismatch", + "token_index": 2 + } + }, + { + "name": "child_jwk_missing", + "note": "", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImZvcmFfcGFyZW50IjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNkltWnZjbUV0WkdWc1pXZGhkR2x2Yml0cWQzUWlmUS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVaWGhoYlhCc1pTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaVdHUjNZWEpFTkV3d01WQlpObEZhTUhSaE16QnBia0pNUjNaVU5UZHBkRTFVV1Vsa01raDZRM0ZTUlNKOUxDSmxlSEFpT2pFM09EQXpNVGc0TURBc0ltbHpjeUk2SW0xaGNtdGxkR1JoZEdFdVpYaGhiWEJzWlNJc0luTmpiM0JsSWpvaWNYVnZkR1U2S2lKOS5va0dIanBoMHFCZTFvMnZSNmZ4UmEyRkdScnh0Qi1hVHZnTU1yODBZQXliV0hpMjRpOEhvLUZnSzRxWTl6ZGZUV0lrajhNem1jRnhTWG53eFBDTG9BZyIsImlzcyI6Ilhkd2FyRDRMMDFQWTZRWjB0YTMwaW5CTEd2VDU3aXRNVFlJZDJIekNxUkUiLCJzY29wZSI6InF1b3RlOioifQ.iYPakSwgNV01hbkFkfxYk3lSktWDDinvNVtkhZzF9VGHtAucc0U4gGNjUJ02qCqrWuTSZVy0ytmdRVWUpqdrAA", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "bad_jwk", + "token_index": 2 + } + }, + { + "name": "child_jwk_not_ed25519", + "note": "an EC P-256 jwk", + "token": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImY4M09KM0QyeEYxQmc4dnViOXRMZTFnSE16Vjc2ZThUdXM5dVBIdlJWRVUiLCJ5IjoieF9GRXpSdTltMzZITE5fdHVlNjU5TE5wWFc2cEN5U3Rpa1lqS0lXSTVhMCJ9LCJ0eXAiOiJmb3JhLWRlbGVnYXRpb24rand0In0.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImZvcmFfcGFyZW50IjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNkltWnZjbUV0WkdWc1pXZGhkR2x2Yml0cWQzUWlmUS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVaWGhoYlhCc1pTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaVdHUjNZWEpFTkV3d01WQlpObEZhTUhSaE16QnBia0pNUjNaVU5UZHBkRTFVV1Vsa01raDZRM0ZTUlNKOUxDSmxlSEFpT2pFM09EQXpNVGc0TURBc0ltbHpjeUk2SW0xaGNtdGxkR1JoZEdFdVpYaGhiWEJzWlNJc0luTmpiM0JsSWpvaWNYVnZkR1U2S2lKOS5va0dIanBoMHFCZTFvMnZSNmZ4UmEyRkdScnh0Qi1hVHZnTU1yODBZQXliV0hpMjRpOEhvLUZnSzRxWTl6ZGZUV0lrajhNem1jRnhTWG53eFBDTG9BZyIsImlzcyI6Ilhkd2FyRDRMMDFQWTZRWjB0YTMwaW5CTEd2VDU3aXRNVFlJZDJIekNxUkUiLCJzY29wZSI6InF1b3RlOioifQ.7ZXfvmftUV0C-U8j3uAuWHxULbN3sGkaQC9a7RMImv-CzwhhxrR87iO3fL0vL6VvfFnXqkPy70v1yDq-m5qJDg", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "bad_jwk", + "token_index": 2 + } + }, + { + "name": "root_jwk_ignored", + "note": "a jwk on the root is not read; the configured key verifies it", + "token": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6ImV4NmlHdzZzVUE0cXB5Y2hQajBEZFRzTFVBTzRfYzk1Sl90bl9kYkREeDQifSwidHlwIjoiZm9yYS1kZWxlZ2F0aW9uK2p3dCJ9.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImlzcyI6Im1hcmtldGRhdGEuZXhhbXBsZSIsInNjb3BlIjoicXVvdGU6KiJ9.npfZ-zrbeoqFgXrkfh-7VsXTPO-hCfjzhwjEsNc3tsBLN1AKjGmFJAHW1ThGutlh4-5sFpELmZrhPd8xJnLLBg", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": true, + "root_issuer": "marketdata.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "scopes": [ + "quote:*" + ], + "expires_at": "2026-06-01T13:00:00Z", + "depth": 1 + } + }, + { + "name": "holder_mismatch", + "note": "the terminal cnf.jkt is not the key the request proved", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiNXRCRFN3V3B3RV9kamFZcWxrQmRSTkZ6TjZHZ3pHSmlXbzRhZUk4YkRWVSJ9LCJleHAiOjE3ODAzMTg4MDAsImlzcyI6Im1hcmtldGRhdGEuZXhhbXBsZSIsInNjb3BlIjoicXVvdGU6KiJ9.u4eA6MMcsKIU2SIE0jdPb-VuPN3nKmhhA-8mafZD6h_Uw_qVLZowjYxFS8Z44AUHcpHX-n-sjnsjLrc3C2OjAw", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "holder_mismatch", + "token_index": 1 + } + }, + { + "name": "oversized_token", + "note": "a token over 32768 bytes is refused before parsing", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImZvcmFfYWR2aXNvcnkiOnsicGFkIjoiYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWEifSwiaXNzIjoibWFya2V0ZGF0YS5leGFtcGxlIiwic2NvcGUiOiJxdW90ZToqIn0.9d98C-J-l_IUqLXd_tSfN8f0mqWcf1oPq7j9qVu2smRj2fVpzZ95bsAE81bLxh1VoVoJ2h0FXs9DfObO4BBdDQ", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "oversized" + } + }, + { + "name": "child_scope_outside_parent", + "note": "", + "token": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6InlGMFNkcUx2R2Z5SFY0TFdoTE0tZ014N3JQZ19MTXhaS3BwSmRZN25SdlkifSwidHlwIjoiZm9yYS1kZWxlZ2F0aW9uK2p3dCJ9.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImZvcmFfcGFyZW50IjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNkltWnZjbUV0WkdWc1pXZGhkR2x2Yml0cWQzUWlmUS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVaWGhoYlhCc1pTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaVdHUjNZWEpFTkV3d01WQlpObEZhTUhSaE16QnBia0pNUjNaVU5UZHBkRTFVV1Vsa01raDZRM0ZTUlNKOUxDSmxlSEFpT2pFM09EQXpNVGc0TURBc0ltbHpjeUk2SW0xaGNtdGxkR1JoZEdFdVpYaGhiWEJzWlNJc0luTmpiM0JsSWpvaWNYVnZkR1U2S2lKOS5va0dIanBoMHFCZTFvMnZSNmZ4UmEyRkdScnh0Qi1hVHZnTU1yODBZQXliV0hpMjRpOEhvLUZnSzRxWTl6ZGZUV0lrajhNem1jRnhTWG53eFBDTG9BZyIsImlzcyI6Ilhkd2FyRDRMMDFQWTZRWjB0YTMwaW5CTEd2VDU3aXRNVFlJZDJIekNxUkUiLCJzY29wZSI6ImNyZWRpdDpyZWFkIn0.P7lJnGk0_S7uHrGArFGgyTambPNBSGbA1MsvCsthTVVjp6ahFFCkAogOX6IlNV7TA6V4LYGrMtp3o-QDXJQlDg", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "scope_widened", + "token_index": 2 + } + }, + { + "name": "child_narrows_wildcard", + "note": "publisher:news:read under publisher:*:read", + "token": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6InlGMFNkcUx2R2Z5SFY0TFdoTE0tZ014N3JQZ19MTXhaS3BwSmRZN25SdlkifSwidHlwIjoiZm9yYS1kZWxlZ2F0aW9uK2p3dCJ9.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImZvcmFfcGFyZW50IjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNkltWnZjbUV0WkdWc1pXZGhkR2x2Yml0cWQzUWlmUS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVaWGhoYlhCc1pTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaVdHUjNZWEpFTkV3d01WQlpObEZhTUhSaE16QnBia0pNUjNaVU5UZHBkRTFVV1Vsa01raDZRM0ZTUlNKOUxDSmxlSEFpT2pFM09EQXpNVGc0TURBc0ltbHpjeUk2SW0xaGNtdGxkR1JoZEdFdVpYaGhiWEJzWlNJc0luTmpiM0JsSWpvaWNIVmliR2x6YUdWeU9pbzZjbVZoWkNKOS5CQWV5ZXV5YlJwSVZNVVlqRVo1TnJpS1pBSnQ0eHdEbjdLcWpOdEtzQnhzS0daeHdCRWlnUi14SUotZFdxcjJhQjRtWm9ZZDBkalpBYjVTbFljdUNDdyIsImlzcyI6Ilhkd2FyRDRMMDFQWTZRWjB0YTMwaW5CTEd2VDU3aXRNVFlJZDJIekNxUkUiLCJzY29wZSI6InB1Ymxpc2hlcjpuZXdzOnJlYWQifQ.tHDdFptIwHOHwEEc6uGKfBhp_DjBY5h9riFwq-kU2rY4SBtTTWGiXV4ipYWIkp5bTlhq1MDOFcSKXKPdUAsLBg", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": true, + "root_issuer": "marketdata.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "scopes": [ + "publisher:news:read" + ], + "expires_at": "2026-06-01T13:00:00Z", + "depth": 2 + } + }, + { + "name": "child_wildcard_widens_parent", + "note": "publisher:news:* under publisher:*:read", + "token": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6InlGMFNkcUx2R2Z5SFY0TFdoTE0tZ014N3JQZ19MTXhaS3BwSmRZN25SdlkifSwidHlwIjoiZm9yYS1kZWxlZ2F0aW9uK2p3dCJ9.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImZvcmFfcGFyZW50IjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNkltWnZjbUV0WkdWc1pXZGhkR2x2Yml0cWQzUWlmUS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVaWGhoYlhCc1pTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaVdHUjNZWEpFTkV3d01WQlpObEZhTUhSaE16QnBia0pNUjNaVU5UZHBkRTFVV1Vsa01raDZRM0ZTUlNKOUxDSmxlSEFpT2pFM09EQXpNVGc0TURBc0ltbHpjeUk2SW0xaGNtdGxkR1JoZEdFdVpYaGhiWEJzWlNJc0luTmpiM0JsSWpvaWNIVmliR2x6YUdWeU9pbzZjbVZoWkNKOS5CQWV5ZXV5YlJwSVZNVVlqRVo1TnJpS1pBSnQ0eHdEbjdLcWpOdEtzQnhzS0daeHdCRWlnUi14SUotZFdxcjJhQjRtWm9ZZDBkalpBYjVTbFljdUNDdyIsImlzcyI6Ilhkd2FyRDRMMDFQWTZRWjB0YTMwaW5CTEd2VDU3aXRNVFlJZDJIekNxUkUiLCJzY29wZSI6InB1Ymxpc2hlcjpuZXdzOioifQ.AJMJm2BBjsHtBnoMhd5mN5SZp0YI8HMfG0exiurB3My9yOBWf-Cdwnb6ellMbusY2wQ17atc7JLDFmyZkWGvAg", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "scope_widened", + "token_index": 2 + } + }, + { + "name": "child_without_scope", + "note": "a child with no scope grants no scoped access and is still valid", + "token": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6InlGMFNkcUx2R2Z5SFY0TFdoTE0tZ014N3JQZ19MTXhaS3BwSmRZN25SdlkifSwidHlwIjoiZm9yYS1kZWxlZ2F0aW9uK2p3dCJ9.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImZvcmFfcGFyZW50IjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNkltWnZjbUV0WkdWc1pXZGhkR2x2Yml0cWQzUWlmUS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVaWGhoYlhCc1pTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaVdHUjNZWEpFTkV3d01WQlpObEZhTUhSaE16QnBia0pNUjNaVU5UZHBkRTFVV1Vsa01raDZRM0ZTUlNKOUxDSmxlSEFpT2pFM09EQXpNVGc0TURBc0ltbHpjeUk2SW0xaGNtdGxkR1JoZEdFdVpYaGhiWEJzWlNJc0luTmpiM0JsSWpvaWNYVnZkR1U2S2lKOS5va0dIanBoMHFCZTFvMnZSNmZ4UmEyRkdScnh0Qi1hVHZnTU1yODBZQXliV0hpMjRpOEhvLUZnSzRxWTl6ZGZUV0lrajhNem1jRnhTWG53eFBDTG9BZyIsImlzcyI6Ilhkd2FyRDRMMDFQWTZRWjB0YTMwaW5CTEd2VDU3aXRNVFlJZDJIekNxUkUifQ.wBZa9HgDd3XzSQp1lePZq_jbst0X81IopIf8HBj8hpNFx5Ez7NLpeNv6z2Za8GWPGe23CW0jCkUDfKHuAp5CDw", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": true, + "root_issuer": "marketdata.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "expires_at": "2026-06-01T13:00:00Z", + "depth": 2 + } + }, + { + "name": "child_exp_later_than_parent", + "note": "", + "token": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6InlGMFNkcUx2R2Z5SFY0TFdoTE0tZ014N3JQZ19MTXhaS3BwSmRZN25SdlkifSwidHlwIjoiZm9yYS1kZWxlZ2F0aW9uK2p3dCJ9.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDEsImZvcmFfcGFyZW50IjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNkltWnZjbUV0WkdWc1pXZGhkR2x2Yml0cWQzUWlmUS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVaWGhoYlhCc1pTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaVdHUjNZWEpFTkV3d01WQlpObEZhTUhSaE16QnBia0pNUjNaVU5UZHBkRTFVV1Vsa01raDZRM0ZTUlNKOUxDSmxlSEFpT2pFM09EQXpNVGc0TURBc0ltbHpjeUk2SW0xaGNtdGxkR1JoZEdFdVpYaGhiWEJzWlNJc0luTmpiM0JsSWpvaWNYVnZkR1U2S2lKOS5va0dIanBoMHFCZTFvMnZSNmZ4UmEyRkdScnh0Qi1hVHZnTU1yODBZQXliV0hpMjRpOEhvLUZnSzRxWTl6ZGZUV0lrajhNem1jRnhTWG53eFBDTG9BZyIsImlzcyI6Ilhkd2FyRDRMMDFQWTZRWjB0YTMwaW5CTEd2VDU3aXRNVFlJZDJIekNxUkUiLCJzY29wZSI6InF1b3RlOioifQ.vIzVGAcbriclQLQWNYGoVzXR6tI-2lH5CP5WfAmTlTE9g7tnoVDDeihBliD2CebXFoH4BxfqE-xf4GOl0Y1bAQ", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "expiry_widened", + "token_index": 2 + } + }, + { + "name": "spend_cap_unsupported", + "note": "this verifier enforces no cap, so a cap claim is refused rather than dropped", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImZvcmFfbWF4X3NwZW5kX2NlbnRzIjo1MDAwMCwiaXNzIjoibWFya2V0ZGF0YS5leGFtcGxlIiwic2NvcGUiOiJxdW90ZToqIn0.8UlkoZ_lWnT1ervHkTRUR331k6EaOgC2MQwhJdQVKsA6LCGdKXRWrS9tkj48cHcjMFP3i956jJebhu4QtWVYCQ", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "unsupported_restriction" + } + }, + { + "name": "unknown_claim", + "note": "a claim the profile does not list is a restriction the verifier cannot evaluate", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImlzcyI6Im1hcmtldGRhdGEuZXhhbXBsZSIsInNjb3BlIjoicXVvdGU6KiIsInZlbmRvcjpwbGFuIjoiZ29sZCJ9.R29GZ6ZvSGgPDBdSe4s7YmnXThNhl1-FtKeOcCB3_l_jQb3x_GGqtNkeog_Js6tOy_D1dY5HQlhAUJHHi-aOAA", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "unknown_claim" + } + }, + { + "name": "root_cap_unsupported", + "note": "a cap on the root under a clean child: claims are checked on every token", + "token": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6InlGMFNkcUx2R2Z5SFY0TFdoTE0tZ014N3JQZ19MTXhaS3BwSmRZN25SdlkifSwidHlwIjoiZm9yYS1kZWxlZ2F0aW9uK2p3dCJ9.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImZvcmFfcGFyZW50IjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNkltWnZjbUV0WkdWc1pXZGhkR2x2Yml0cWQzUWlmUS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVaWGhoYlhCc1pTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaVdHUjNZWEpFTkV3d01WQlpObEZhTUhSaE16QnBia0pNUjNaVU5UZHBkRTFVV1Vsa01raDZRM0ZTUlNKOUxDSmxlSEFpT2pFM09EQXpNVGc0TURBc0ltWnZjbUZmYldGNFgzTndaVzVrWDJObGJuUnpJam8xTURBd01Dd2lhWE56SWpvaWJXRnlhMlYwWkdGMFlTNWxlR0Z0Y0d4bElpd2ljMk52Y0dVaU9pSnhkVzkwWlRvcUluMC5fSU9fU2lkZ1Y4ZEpvLUM0SFJTa1BITjQtVFV5RU9yaWF4OUttOTZSUlJib2hIdnZZWmFoYjlTQXZYWlVDZHVJUGl6UDFhSEVXdzFBU0l1bTNjTU9BQSIsImlzcyI6Ilhkd2FyRDRMMDFQWTZRWjB0YTMwaW5CTEd2VDU3aXRNVFlJZDJIekNxUkUiLCJzY29wZSI6InF1b3RlOioifQ.MkEsj27SitBICg5JWhFbHFEaHC2mxahW4p8KVVPAMMAvXEfKLEmIrahtG-DB8NQL1CVMJi9dpiLBtS8u3HiSBg", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "unsupported_restriction" + } + }, + { + "name": "root_unknown_claim", + "note": "an unknown claim on the root under a clean child", + "token": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6InlGMFNkcUx2R2Z5SFY0TFdoTE0tZ014N3JQZ19MTXhaS3BwSmRZN25SdlkifSwidHlwIjoiZm9yYS1kZWxlZ2F0aW9uK2p3dCJ9.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImZvcmFfcGFyZW50IjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNkltWnZjbUV0WkdWc1pXZGhkR2x2Yml0cWQzUWlmUS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVaWGhoYlhCc1pTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaVdHUjNZWEpFTkV3d01WQlpObEZhTUhSaE16QnBia0pNUjNaVU5UZHBkRTFVV1Vsa01raDZRM0ZTUlNKOUxDSmxlSEFpT2pFM09EQXpNVGc0TURBc0ltbHpjeUk2SW0xaGNtdGxkR1JoZEdFdVpYaGhiWEJzWlNJc0luTmpiM0JsSWpvaWNYVnZkR1U2S2lJc0luWmxibVJ2Y2pwd2JHRnVJam9pWjI5c1pDSjkubHl3ZDN4b2tac2dZMGlQOW9SSDJfa0o2VXU1TndBTm14WUdmV3pHYzNkZERyUGNjOGZSMWk0UGU3dGlJeURmSGRuRUhENnlHUFJxb0EtcHpVNnBMQmciLCJpc3MiOiJYZHdhckQ0TDAxUFk2UVowdGEzMGluQkxHdlQ1N2l0TVRZSWQySHpDcVJFIiwic2NvcGUiOiJxdW90ZToqIn0.40RbXxuMO4jDh1OorN44ZI4Ec66sZlQyH932_WFBor-gavj9P-8FF-7v3Y7Fh0_TUWEQl2Zp8Q-xAjrgZTNACw", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "unknown_claim" + } + }, + { + "name": "middle_cap_unsupported", + "note": "a cap on the middle token of three", + "token": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im8wOEhqQV9XOTM5Y2JjdWxNUkpNc243d3FUSkFQNnZKaWJ6YTlEejl0UTQifSwidHlwIjoiZm9yYS1kZWxlZ2F0aW9uK2p3dCJ9.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImZvcmFfcGFyZW50IjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJbXAzYXlJNmV5SmpjbllpT2lKRlpESTFOVEU1SWl3aWEzUjVJam9pVDB0UUlpd2llQ0k2SW5sR01GTmtjVXgyUjJaNVNGWTBURmRvVEUwdFowMTROM0pRWjE5TVRYaGFTM0J3U21SWk4yNVNkbGtpZlN3aWRIbHdJam9pWm05eVlTMWtaV3hsWjJGMGFXOXVLMnAzZENKOS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVaWGhoYlhCc1pTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaU5YUkNSRk4zVjNCM1JWOWthbUZaY1d4clFtUlNUa1o2VGpaSFozcEhTbWxYYnpSaFpVazRZa1JXVlNKOUxDSmxlSEFpT2pFM09EQXpNVGc0TURBc0ltWnZjbUZmYldGNFgyRmpZMlZ6YzJWeklqb3hNQ3dpWm05eVlWOXdZWEpsYm5RaU9pSmxlVXBvWWtkamFVOXBTa1phUlZKVVVWTkpjMGx1VWpWalEwazJTVzFhZG1OdFJYUmFSMVp6V2xka2FHUkhiSFppYVhSeFpETlJhV1pSTG1WNVNtaGtWMUZwVDJ4emFWcFlhR3BoUjBaMVdqSlZkVnBZYUdoaVdFSnpXbE5LWkV4RFNtcGliVmxwVDI1emFXRnRkREJKYW05cFYwZFNNMWxZU2tWT1JYZDNUVlpDV2s1c1JtRk5TRkpvVFhwQ2NHSnJTazFTTTFwVlRsUmtjR1JGTVZWWFZXeHJUV3RvTmxFelJsTlNVMG81VEVOS2JHVklRV2xQYWtVelQwUkJlazFVWnpSTlJFRnpTVzFzZW1ONVNUWkpiVEZvWTIxMGJHUkhVbWhrUjBWMVdsaG9hR0pZUW5OYVUwbHpTVzVPYW1JelFteEphbTlwWTFoV2RtUkhWVFpMYVVvNUxtOXJSMGhxY0dnd2NVSmxNVzh5ZGxJMlpuaFNZVEpHUjFKeWVIUkNMV0ZVZG1kTlRYSTRNRmxCZVdKWFNHa3lOR2s0U0c4dFJtZExOSEZaT1hwa1psUlhTV3RxT0UxNmJXTkdlRk5ZYm5kNFVFTk1iMEZuSWl3aWFYTnpJam9pV0dSM1lYSkVORXd3TVZCWk5sRmFNSFJoTXpCcGJrSk1SM1pVTlRkcGRFMVVXVWxrTWtoNlEzRlNSU0lzSW5OamIzQmxJam9pY1hWdmRHVTZLaUo5LjhsU182ZVNseFcyZk1BU2g5ZWFhM1hDM2VJVDAtRkJUY0pNbVFadkttZzVpR3N0QzVvODJza2wtYmlHc3B4RTFuU1hVQmdCVkZpWmQweEpSY2p3U0FRIiwiaXNzIjoiNXRCRFN3V3B3RV9kamFZcWxrQmRSTkZ6TjZHZ3pHSmlXbzRhZUk4YkRWVSIsInNjb3BlIjoicXVvdGU6KiJ9._koA3STLQINlc4TDYEp-Ba_dP4guVBUnEQKQRow5wc31CZH4cxDbxEshX53AiDIz9MT-ofYRTD7mOt1k0lAHCw", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "unsupported_restriction" + } + }, + { + "name": "middle_unknown_claim", + "note": "an unknown claim on the middle token of three", + "token": "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im8wOEhqQV9XOTM5Y2JjdWxNUkpNc243d3FUSkFQNnZKaWJ6YTlEejl0UTQifSwidHlwIjoiZm9yYS1kZWxlZ2F0aW9uK2p3dCJ9.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImZvcmFfcGFyZW50IjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJbXAzYXlJNmV5SmpjbllpT2lKRlpESTFOVEU1SWl3aWEzUjVJam9pVDB0UUlpd2llQ0k2SW5sR01GTmtjVXgyUjJaNVNGWTBURmRvVEUwdFowMTROM0pRWjE5TVRYaGFTM0J3U21SWk4yNVNkbGtpZlN3aWRIbHdJam9pWm05eVlTMWtaV3hsWjJGMGFXOXVLMnAzZENKOS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVaWGhoYlhCc1pTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaU5YUkNSRk4zVjNCM1JWOWthbUZaY1d4clFtUlNUa1o2VGpaSFozcEhTbWxYYnpSaFpVazRZa1JXVlNKOUxDSmxlSEFpT2pFM09EQXpNVGc0TURBc0ltWnZjbUZmY0dGeVpXNTBJam9pWlhsS2FHSkhZMmxQYVVwR1drVlNWRkZUU1hOSmJsSTFZME5KTmtsdFduWmpiVVYwV2tkV2MxcFhaR2hrUjJ4MlltbDBjV1F6VVdsbVVTNWxlVXBvWkZkUmFVOXNjMmxhV0docVlVZEdkVm95VlhWYVdHaG9ZbGhDYzFwVFNtUk1RMHBxWW0xWmFVOXVjMmxoYlhRd1NXcHZhVmRIVWpOWldFcEZUa1YzZDAxV1FscE9iRVpoVFVoU2FFMTZRbkJpYTBwTlVqTmFWVTVVWkhCa1JURlZWMVZzYTAxcmFEWlJNMFpUVWxOS09VeERTbXhsU0VGcFQycEZNMDlFUVhwTlZHYzBUVVJCYzBsdGJIcGplVWsyU1cweGFHTnRkR3hrUjFKb1pFZEZkVnBZYUdoaVdFSnpXbE5KYzBsdVRtcGlNMEpzU1dwdmFXTllWblprUjFVMlMybEtPUzV2YTBkSWFuQm9NSEZDWlRGdk1uWlNObVo0VW1FeVJrZFNjbmgwUWkxaFZIWm5UVTF5T0RCWlFYbGlWMGhwTWpScE9FaHZMVVpuU3pSeFdUbDZaR1pVVjBscmFqaE5lbTFqUm5oVFdHNTNlRkJEVEc5Qlp5SXNJbWx6Y3lJNklsaGtkMkZ5UkRSTU1ERlFXVFpSV2pCMFlUTXdhVzVDVEVkMlZEVTNhWFJOVkZsSlpESklla054VWtVaUxDSnpZMjl3WlNJNkluRjFiM1JsT2lvaUxDSjJaVzVrYjNJNmNHeGhiaUk2SW1kdmJHUWlmUS5SRm5oenF1eVI1QlpLOXlrcmhUTEM2TGJOelU4SzdGODlHaHZwZkptNk9UUHJzWWdQQmxYdnRIY0R5X1J3MkNNMGhGa0VCVUtVbUlRSjg3RzhSRDZCQSIsImlzcyI6IjV0QkRTd1dwd0VfZGphWXFsa0JkUk5Gek42R2d6R0ppV280YWVJOGJEVlUiLCJzY29wZSI6InF1b3RlOioifQ.2hTNoSuNR5hYAoPuIJLz5iSI246ya9l-LCd_r4d31Ydvk99zuoqLTnPxVdUSI_9JVCsf3fnCzbGWolgfBGzGAg", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "unknown_claim" + } + }, + { + "name": "advisory_unknown_content_ignored", + "note": "", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImZvcmFfYWR2aXNvcnkiOnsicGxhbiI6eyJ0aWVyIjoyfSwidmVuZG9yIjoieCJ9LCJpc3MiOiJtYXJrZXRkYXRhLmV4YW1wbGUiLCJzY29wZSI6InF1b3RlOioifQ.RUsfC-bWuuHhIYCelTq_7zas5caJLiR3l_cWt-qaElyB6WgQdW8zmRT2vvDiXf9ul_j407XBXVXxJgCWa_j-CA", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": true, + "root_issuer": "marketdata.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "scopes": [ + "quote:*" + ], + "expires_at": "2026-06-01T13:00:00Z", + "depth": 1 + } + }, + { + "name": "advisory_shadows_profile_claim", + "note": "scope inside fora_advisory", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImZvcmFfYWR2aXNvcnkiOnsic2NvcGUiOiIqIn0sImlzcyI6Im1hcmtldGRhdGEuZXhhbXBsZSIsInNjb3BlIjoicXVvdGU6KiJ9.lBCvQcOX-G7roLVU2sA_aoowj4BDEFhA1snzRuJWeVZiemtW37IJsZ54KRn8Z20xWuTA_th2MwTnR1hcYah7BA", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "advisory_shadows_claim" + } + }, + { + "name": "mirrors_disagree_no_effect", + "note": "plaintext Delegation fields that disagree with the signed chain change nothing; an Exchange verifies the chain and never reads them for trust", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImlzcyI6Im1hcmtldGRhdGEuZXhhbXBsZSIsInNjb3BlIjoicXVvdGU6KiJ9.pr2ux5pWGLBl8t5SDdj4EyOUBkaH_WyRqR3DR2xawwuTAK64Ky13IdrilNJNrhNlZlHHJpm58p15dU0uCxHXAA", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "mirrors": { + "issuer": "attacker.example", + "principal_domain": "attacker.example", + "scopes": [ + "*" + ], + "expires_at": "2099-01-01T00:00:00Z" + }, + "expect": { + "valid": true, + "root_issuer": "marketdata.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "scopes": [ + "quote:*" + ], + "expires_at": "2026-06-01T13:00:00Z", + "depth": 1 + } + }, + { + "name": "raw_baseline", + "note": "the raw-signed root every raw syntax case starts from", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJpc3MiOiJtYXJrZXRkYXRhLmV4YW1wbGUiLCJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJleHAiOjE3ODAzMTg4MDAsInNjb3BlIjoicXVvdGU6KiIsImNuZiI6eyJqa3QiOiJXaS12TlR2Tzg0cG9kSnBJdlhLVWxHTkRVdWhLSktTNHpWemhuUnZUM1k0In19.RaDmXu0At-6BCwtbBaVhSHLRfRoVLjW7Xx1jkk3TnPL8_E140yPlCgewpohI13OcLYJDqGIcYhGV35tZZbWGAw", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": true, + "root_issuer": "marketdata.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "scopes": [ + "quote:*" + ], + "expires_at": "2026-06-01T13:00:00Z", + "depth": 1 + } + }, + { + "name": "payload_trailing_object", + "note": "a second JSON object after the claims object, under a valid signature", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJpc3MiOiJtYXJrZXRkYXRhLmV4YW1wbGUiLCJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJleHAiOjE3ODAzMTg4MDAsInNjb3BlIjoicXVvdGU6KiIsImNuZiI6eyJqa3QiOiJXaS12TlR2Tzg0cG9kSnBJdlhLVWxHTkRVdWhLSktTNHpWemhuUnZUM1k0In19IHsic2NvcGUiOiIqIn0.jL1reIpgSllgn-xhmiKCbOD0jt321zBRHFhdCddyuxazWfV2cR3fca5N4lQVv4sPKb9V-hWqbPuY9aVNQsN-DQ", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "malformed" + } + }, + { + "name": "payload_trailing_text", + "note": "text after the claims object, under a valid signature", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJpc3MiOiJtYXJrZXRkYXRhLmV4YW1wbGUiLCJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJleHAiOjE3ODAzMTg4MDAsInNjb3BlIjoicXVvdGU6KiIsImNuZiI6eyJqa3QiOiJXaS12TlR2Tzg0cG9kSnBJdlhLVWxHTkRVdWhLSktTNHpWemhuUnZUM1k0In19anVuaw.mdD9Ivx3dXaHf_1QOA3Z45Id4nUfasPXj_jLTPM5vMpFGzdN8D2zsuRQ1IbnkPQusgJc8lNJBjtcW9VPbjEXAg", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "malformed" + } + }, + { + "name": "payload_not_object", + "note": "a JSON array as payload", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.WzFd.PsabWxjdcznvpOlb5u-kTl781Kl8pj8ACe7Ry1S1JEI7-gXIlsUoaXwurpr77PpH0mJbfsR3ssSVEgPBbL9-Bw", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "malformed" + } + }, + { + "name": "header_b64_false", + "note": "RFC 7797 b64:false without crit", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QiLCJiNjQiOmZhbHNlfQ.eyJpc3MiOiJtYXJrZXRkYXRhLmV4YW1wbGUiLCJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJleHAiOjE3ODAzMTg4MDAsInNjb3BlIjoicXVvdGU6KiIsImNuZiI6eyJqa3QiOiJXaS12TlR2Tzg0cG9kSnBJdlhLVWxHTkRVdWhLSktTNHpWemhuUnZUM1k0In19.nQtr-rlnLsFjrzxb3prW3H_kQ8nX5Zs4gyWzIXyUBvRC90-VtpT9u21ogQRXMKMKFNqZZN3CxXh4GpGtxXLTDA", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "malformed" + } + }, + { + "name": "signature_crlf", + "note": "CR LF appended to the signature segment", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJpc3MiOiJtYXJrZXRkYXRhLmV4YW1wbGUiLCJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJleHAiOjE3ODAzMTg4MDAsInNjb3BlIjoicXVvdGU6KiIsImNuZiI6eyJqa3QiOiJXaS12TlR2Tzg0cG9kSnBJdlhLVWxHTkRVdWhLSktTNHpWemhuUnZUM1k0In19.RaDmXu0At-6BCwtbBaVhSHLRfRoVLjW7Xx1jkk3TnPL8_E140yPlCgewpohI13OcLYJDqGIcYhGV35tZZbWGAw\r\n", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "malformed" + } + }, + { + "name": "signature_padded", + "note": "a padding character in the signature segment", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJpc3MiOiJtYXJrZXRkYXRhLmV4YW1wbGUiLCJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJleHAiOjE3ODAzMTg4MDAsInNjb3BlIjoicXVvdGU6KiIsImNuZiI6eyJqa3QiOiJXaS12TlR2Tzg0cG9kSnBJdlhLVWxHTkRVdWhLSktTNHpWemhuUnZUM1k0In19.RaDmXu0At-6BCwtbBaVhSHLRfRoVLjW7Xx1jkk3TnPL8_E140yPlCgewpohI13OcLYJDqGIcYhGV35tZZbWGAw=", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "malformed" + } + }, + { + "name": "exp_beyond_numeric_date_range", + "note": "exp 1e100 is finite but no date the profile can return", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJpc3MiOiJtYXJrZXRkYXRhLmV4YW1wbGUiLCJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJleHAiOjFlMTAwLCJzY29wZSI6InF1b3RlOioiLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9fQ.GGyNSSAUCrX56_o-0T7l-_QQmlh1KfQwl1BMHz-5QBvv7kak31ehILTOipUEI1dUJaG3lSrTWVo1Trpg8dH9Bg", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "bad_claim" + } + }, + { + "name": "exp_negative", + "note": "", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJpc3MiOiJtYXJrZXRkYXRhLmV4YW1wbGUiLCJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJleHAiOi0xLCJzY29wZSI6InF1b3RlOioiLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9fQ.l0xObm_vpP6v7ZMkm0ythyuzRpSt4x7L8indSGxrhg_Gl1MqwfTugPsNhDLZJpoQoawfnj5WiNRunyxsIM1kAA", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": false, + "failure": "bad_claim" + } + }, + { + "name": "exp_at_numeric_date_max", + "note": "253402300799 is the last accepted second", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJpc3MiOiJtYXJrZXRkYXRhLmV4YW1wbGUiLCJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJleHAiOjI1MzQwMjMwMDc5OSwic2NvcGUiOiJxdW90ZToqIiwiY25mIjp7ImprdCI6IldpLXZOVHZPODRwb2RKcEl2WEtVbEdORFV1aEtKS1M0elZ6aG5SdlQzWTQifX0.s1Xu2NMIfF2ZM0Lp14nk_Hx0pZs2xHN4ZNlkSbL9ofiBgjOEkmbUSkltK3aH8ZZ5kXpqdFJGYRXC4gdAPwmDBg", + "trusted": [ + { + "identity": "marketdata.example", + "key_x": "C0eCPnEJXdWb54rCccV27zifh7ZFYasHz5pOvNAtIEE" + } + ], + "audience": "exchange.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": true, + "root_issuer": "marketdata.example", + "holder": "Wi-vNTvO84podJpIvXKUlGNDUuhKJKS4zVzhnRvT3Y4", + "scopes": [ + "quote:*" + ], + "expires_at": "9999-12-31T23:59:59Z", + "depth": 1 + } + }, + { + "name": "docs_subscription_agent", + "note": "authentication page, Subscription Agent example: a MarketData root token bound to FinBot's request key", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoibTBMODhtcUx3MnZzRnpMY0FHVGlNdWlOajY3RjF0QXlBb2RoOW13S3p4OCJ9LCJleHAiOjE3ODAzMTg4MDAsImlzcyI6Im1hcmtldGRhdGEuZXhhbXBsZS5jb20iLCJqdGkiOiJtZC1zdWItMjAyNi0wNDgxIiwic2NvcGUiOiJzdWJzY3JpcHRpb246bWFya2V0ZGF0YS0yMDI2IHF1b3RlOiogZWFybmluZ3M6KiJ9.d7R4gaXcCgy9nR304l_KxO9sQEWpKDk8uV4PEnIsze-Qnr_J0_9__m3gW8bGFeibWezuTB8jF2G7JlelimohAg", + "trusted": [ + { + "identity": "marketdata.example.com", + "key_x": "dPyio7OJ-xpk2b9SzA3UwpZPOATAz3x1XoUTxtuBmNw" + } + ], + "audience": "exchange.example", + "holder": "m0L88mqLw2vsFzLcAGTiMuiNj67F1tAyAodh9mwKzx8", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": true, + "root_issuer": "marketdata.example.com", + "holder": "m0L88mqLw2vsFzLcAGTiMuiNj67F1tAyAodh9mwKzx8", + "scopes": [ + "earnings:*", + "quote:*", + "subscription:marketdata-2026" + ], + "expires_at": "2026-06-01T13:00:00Z", + "depth": 1 + } + }, + { + "name": "docs_enterprise_agent", + "note": "authentication page, Enterprise Agent example: an IAM root token bound to the legal research agent's key", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiRmVmSksyT0taSmJCUlljemdMOHlZazRrUzVUSF9UUmoxd3I3R1JyaTMyNCJ9LCJleHAiOjE3ODAzMTg4MDAsImlzcyI6ImlhbS5raXJrbGFuZC5jb20iLCJqdGkiOiJpYW0tN2MxZSIsInNjb3BlIjoibGVnYWw6Y2FzZS1sYXcgbGVnYWw6c3RhdHV0ZXMgY3JlZGl0OnJlYWQiLCJzdWIiOiJ1c2VyQGtpcmtsYW5kLmNvbSJ9.r6naxJkO-OaCSG3lCgasFEuEP9-lx7PSCUMrtmikP9a-knEgu9rGQpbOdU7OmiisbdoTXOX7HHxvll7ZstTMCQ", + "trusted": [ + { + "identity": "iam.kirkland.com", + "key_x": "f9aRjdu6IDj0_FWvwovQxY99vRgm_2PyKfwWCN2Mt5c" + } + ], + "audience": "exchange.example", + "holder": "FefJK2OKZJbBRYczgL8yYk4kS5TH_TRj1wr7GRri324", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": true, + "root_issuer": "iam.kirkland.com", + "holder": "FefJK2OKZJbBRYczgL8yYk4kS5TH_TRj1wr7GRri324", + "scopes": [ + "credit:read", + "legal:case-law", + "legal:statutes" + ], + "expires_at": "2026-06-01T13:00:00Z", + "depth": 1 + } + }, + { + "name": "docs_delegated_sub_agent", + "note": "authentication page, Delegated Sub-Agent example: the legal agent narrows the IAM grant to credit:read for a sub-agent, one hour shorter", + "token": "eyJhbGciOiJFZDI1NTE5IiwiandrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoiZGtVeVJWNmttN1pDVkpZWWZsUTMwMXFZcmJ6NDJvZGhyYlVSWHFkbVRzOCJ9LCJ0eXAiOiJmb3JhLWRlbGVnYXRpb24rand0In0.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiVmliRjMyQkM1T2VyRHYtSjNtcDFYQ3V3bzk3WFJDSi1FdVNKbGxKS0JxVSJ9LCJleHAiOjE3ODAzMTcwMDAsImZvcmFfcGFyZW50IjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNkltWnZjbUV0WkdWc1pXZGhkR2x2Yml0cWQzUWlmUS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVaWGhoYlhCc1pTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaVJtVm1Ta3N5VDB0YVNtSkNVbGxqZW1kTU9IbFphelJyVXpWVVNGOVVVbW94ZDNJM1IxSnlhVE15TkNKOUxDSmxlSEFpT2pFM09EQXpNVGc0TURBc0ltbHpjeUk2SW1saGJTNXJhWEpyYkdGdVpDNWpiMjBpTENKcWRHa2lPaUpwWVcwdE4yTXhaU0lzSW5OamIzQmxJam9pYkdWbllXdzZZMkZ6WlMxc1lYY2diR1ZuWVd3NmMzUmhkSFYwWlhNZ1kzSmxaR2wwT25KbFlXUWlMQ0p6ZFdJaU9pSjFjMlZ5UUd0cGNtdHNZVzVrTG1OdmJTSjkucjZuYXhKa08tT2FDU0czbENnYXNGRXVFUDktbHg3UFNDVU1ydG1pa1A5YS1rbkVndTlyR1FwYk9kVTdPbWlpc2Jkb1RYT1g3SEh4dmxsN1pzdFRNQ1EiLCJpc3MiOiJGZWZKSzJPS1pKYkJSWWN6Z0w4eVlrNGtTNVRIX1RSajF3cjdHUnJpMzI0IiwianRpIjoibGVnYWwtc3ViLTAwMDciLCJzY29wZSI6ImNyZWRpdDpyZWFkIn0.cbAX6Z6n-7iUG4AaA49kSQW4xn-LigUQcwGtQiTEOeCB0aFUlPvNvihP6XNppUZ898n1sN2hICofcsm2BwtXBQ", + "trusted": [ + { + "identity": "iam.kirkland.com", + "key_x": "f9aRjdu6IDj0_FWvwovQxY99vRgm_2PyKfwWCN2Mt5c" + } + ], + "audience": "exchange.example", + "holder": "VibF32BC5OerDv-J3mp1XCuwo97XRCJ-EuSJllJKBqU", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": true, + "root_issuer": "iam.kirkland.com", + "holder": "VibF32BC5OerDv-J3mp1XCuwo97XRCJ-EuSJllJKBqU", + "scopes": [ + "credit:read" + ], + "expires_at": "2026-06-01T12:30:00Z", + "depth": 2 + } + }, + { + "name": "docs_walkthrough_v1", + "note": "walkthrough v1 scenario B: the MarketData root token for the hedge fund's agent, addressed to exchange.marketdata-data.com", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOlsiZXhjaGFuZ2UubWFya2V0ZGF0YS1kYXRhLmNvbSJdLCJjbmYiOnsiamt0IjoiU2ZjeWZUQnpUUmhSby1lMVBlRC1pTC1JX1gtUURsZ0FKZ3ZVeTZON29DRSJ9LCJleHAiOjE3OTg3NjE1OTksImlzcyI6Im1hcmtldGRhdGEuZXhhbXBsZS5jb20iLCJqdGkiOiJtZC1oZi0wMDEtMjAyNiIsInNjb3BlIjoicXVvdGU6KiBlYXJuaW5nczoqIG5ld3M6cmVhZCBzdWJzY3JpcHRpb246ZW50ZXJwcmlzZS1zdWItSEYtMDAxIiwic3ViIjoiZW50ZXJwcmlzZS1zdWItSEYtMDAxIn0.I_vfPKVVHJ0aQFJxjqgyXGy1--0q86P7gef2WSOtiXvCZk_yskXuF-XTy0tTn7jT7vmLYCEe0iNQJbI1oR8bDw", + "trusted": [ + { + "identity": "marketdata.example.com", + "key_x": "dPyio7OJ-xpk2b9SzA3UwpZPOATAz3x1XoUTxtuBmNw" + } + ], + "audience": "exchange.marketdata-data.com", + "holder": "SfcyfTBzTRhRo-e1PeD-iL-I_X-QDlgAJgvUy6N7oCE", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": true, + "root_issuer": "marketdata.example.com", + "holder": "SfcyfTBzTRhRo-e1PeD-iL-I_X-QDlgAJgvUy6N7oCE", + "scopes": [ + "earnings:*", + "news:read", + "quote:*", + "subscription:enterprise-sub-HF-001" + ], + "expires_at": "2026-12-31T23:59:59Z", + "depth": 1 + } + }, + { + "name": "docs_walkthrough_academic", + "note": "academic walkthrough: Stanford's root token for the literature review agent, addressed to exchange.scholarsgate.com", + "token": "eyJhbGciOiJFZERTQSIsInR5cCI6ImZvcmEtZGVsZWdhdGlvbitqd3QifQ.eyJhdWQiOlsiZXhjaGFuZ2Uuc2Nob2xhcnNnYXRlLmNvbSJdLCJjbmYiOnsiamt0IjoiQVNOaUFFU3NQcXU3ZFhNNThMNkNhb0tsTGRFVVhMVG5iUEpraEhpal95RSJ9LCJleHAiOjE3OTg3NjE1OTksImlzcyI6InN0YW5mb3JkLmVkdSIsImp0aSI6Imluc3Qtc3ViLXN0YW5mb3JkLTIwMjYtMDE5MyIsInNjb3BlIjoiYWNhZGVtaWM6KiBzdWJzY3JpcHRpb246aW5zdC1zdWItc3RhbmZvcmQtMjAyNiIsInN1YiI6Imluc3Qtc3ViLXN0YW5mb3JkLTIwMjYifQ.q1otny5g3OvYlg8zrQE6iCYdWxxImOJf64r04r5Z0PaYY69nTUX_MXzBqFfp5HZ4ZM45mkux6AcdVyRUlrP_Cw", + "trusted": [ + { + "identity": "stanford.edu", + "key_x": "5kt46ox_9BiBFTo9HiQjl-7djqeHDWcGzCjkERqlCEs" + } + ], + "audience": "exchange.scholarsgate.com", + "holder": "ASNiAESsPqu7dXM58L6CaoKlLdEUXLTnbPJkhHij_yE", + "now": "2026-06-01T12:00:00Z", + "expect": { + "valid": true, + "root_issuer": "stanford.edu", + "holder": "ASNiAESsPqu7dXM58L6CaoKlLdEUXLTnbPJkhHij_yE", + "scopes": [ + "academic:*", + "subscription:inst-sub-stanford-2026" + ], + "expires_at": "2026-12-31T23:59:59Z", + "depth": 1 + } + } + ] +} diff --git a/sdk/go/helpers/testdata/scope-match-vectors.json b/sdk/go/helpers/testdata/scope-match-vectors.json new file mode 100644 index 00000000..dbe4aa64 --- /dev/null +++ b/sdk/go/helpers/testdata/scope-match-vectors.json @@ -0,0 +1,49 @@ +{ + "covers": [ + {"name": "exact_equal", "granted": "credit:read", "required": "credit:read", "expected": true}, + {"name": "exact_different_permission", "granted": "credit:read", "required": "credit:write", "expected": false}, + {"name": "exact_different_domain", "granted": "credit:read", "required": "debit:read", "expected": false}, + {"name": "case_sensitive", "granted": "Credit:read", "required": "credit:read", "expected": false}, + {"name": "terminal_wildcard_one_segment", "granted": "dist:*", "required": "dist:US", "expected": true}, + {"name": "terminal_wildcard_many_segments", "granted": "dist:*", "required": "dist:US:CA", "expected": true}, + {"name": "terminal_wildcard_other_branch", "granted": "dist:US:*", "required": "dist:EU", "expected": false}, + {"name": "terminal_wildcard_deeper", "granted": "dist:US:*", "required": "dist:US:CA", "expected": true}, + {"name": "terminal_wildcard_needs_one_remaining_segment", "granted": "dist:US:*", "required": "dist:US", "expected": false}, + {"name": "inner_wildcard", "granted": "publisher:*:read", "required": "publisher:news:read", "expected": true}, + {"name": "inner_wildcard_wrong_last_segment", "granted": "publisher:*:read", "required": "publisher:news:write", "expected": false}, + {"name": "inner_wildcard_matches_one_segment_only", "granted": "a:*:c", "required": "a:b:c:d", "expected": false}, + {"name": "leading_wildcard", "granted": "*:read", "required": "credit:read", "expected": true}, + {"name": "bare_segment_covers_itself", "granted": "dist", "required": "dist", "expected": true}, + {"name": "no_implicit_prefix", "granted": "dist", "required": "dist:US", "expected": false}, + {"name": "no_implicit_prefix_two_segments", "granted": "dist:US", "required": "dist:US:CA", "expected": false}, + {"name": "narrower_grant_does_not_cover", "granted": "dist:US:CA", "required": "dist:US", "expected": false}, + {"name": "bare_star_covers_two_segments", "granted": "*", "required": "credit:read", "expected": true}, + {"name": "bare_star_covers_bare_segment", "granted": "*", "required": "dist", "expected": true}, + {"name": "bare_star_covers_star", "granted": "*", "required": "*", "expected": true}, + {"name": "bare_star_covers_wildcard_requirement", "granted": "*", "required": "publisher:*:read", "expected": true}, + {"name": "required_wildcard_is_literal", "granted": "credit:read", "required": "*", "expected": false}, + {"name": "required_terminal_wildcard_is_literal", "granted": "publisher:*:read", "required": "publisher:news:*", "expected": false}, + {"name": "equal_wildcard_scopes", "granted": "publisher:*:read", "required": "publisher:*:read", "expected": true}, + {"name": "terminal_wildcard_covers_narrower_wildcard", "granted": "dist:*", "required": "dist:US:*", "expected": true}, + {"name": "empty_grant_covers_nothing", "granted": "", "required": "credit:read", "expected": false}, + {"name": "empty_requirement_never_covered", "granted": "*", "required": "", "expected": false}, + {"name": "no_trimming", "granted": "credit:read ", "required": "credit:read", "expected": false} + ], + "cover_sets": [ + {"name": "empty_required_of_empty_granted", "granted": [], "required": [], "expected": true}, + {"name": "empty_required_always_covered", "granted": ["credit:read"], "required": [], "expected": true}, + {"name": "empty_granted_covers_nothing", "granted": [], "required": ["credit:read"], "expected": false}, + {"name": "each_required_covered_by_some_grant", "granted": ["dist:*", "credit:read"], "required": ["dist:US", "credit:read"], "expected": true}, + {"name": "one_uncovered_requirement_fails", "granted": ["dist:*"], "required": ["dist:US", "credit:read"], "expected": false}, + {"name": "order_does_not_matter", "granted": ["credit:read", "dist:*"], "required": ["dist:US:CA"], "expected": true}, + {"name": "exact_set_containment", "granted": ["read", "write"], "required": ["read"], "expected": true}, + {"name": "exact_set_missing_member", "granted": ["read", "write"], "required": ["admin"], "expected": false}, + {"name": "parent_inner_wildcard_accepts_narrow_child", "granted": ["publisher:*:read"], "required": ["publisher:news:read"], "expected": true}, + {"name": "parent_inner_wildcard_refuses_child_terminal_wildcard", "granted": ["publisher:*:read"], "required": ["publisher:news:*"], "expected": false}, + {"name": "child_equal_to_parent", "granted": ["publisher:*:read"], "required": ["publisher:*:read"], "expected": true}, + {"name": "child_bare_star_refused_by_narrow_parent", "granted": ["credit:read"], "required": ["*"], "expected": false}, + {"name": "bare_star_parent_accepts_anything", "granted": ["*"], "required": ["credit:read", "dist:US:CA", "publisher:*:read"], "expected": true}, + {"name": "child_narrows_terminal_wildcard", "granted": ["dist:*"], "required": ["dist:US:*", "dist:EU"], "expected": true}, + {"name": "child_adds_segment_under_bare_parent", "granted": ["dist"], "required": ["dist:US"], "expected": false} + ] +} diff --git a/sdk/go/helpers/testdata/scopes-vectors.json b/sdk/go/helpers/testdata/scopes-vectors.json index 83f5f6f9..2b2b2172 100644 --- a/sdk/go/helpers/testdata/scopes-vectors.json +++ b/sdk/go/helpers/testdata/scopes-vectors.json @@ -180,6 +180,66 @@ "read" ], "expected": false + }, + { + "name": "wildcard_super_covers_narrower_sub", + "sub": [ + "publisher:news:read" + ], + "super": [ + "publisher:*:read" + ], + "expected": true + }, + { + "name": "sub_wildcard_wider_than_super", + "sub": [ + "publisher:news:*" + ], + "super": [ + "publisher:*:read" + ], + "expected": false + }, + { + "name": "terminal_wildcard_super_covers_deeper_sub", + "sub": [ + "dist:US:CA" + ], + "super": [ + "dist:*" + ], + "expected": true + }, + { + "name": "sub_bare_star_not_covered_by_narrow_super", + "sub": [ + "*" + ], + "super": [ + "read" + ], + "expected": false + }, + { + "name": "no_implicit_prefix_in_subset", + "sub": [ + "dist:US" + ], + "super": [ + "dist" + ], + "expected": false + }, + { + "name": "sub_longer_than_super_terminal_star", + "sub": [ + "dist:US" + ], + "super": [ + "dist:US:*" + ], + "expected": false } ] } diff --git a/sdk/parity/symbol-map.json b/sdk/parity/symbol-map.json index af753d11..d41ba518 100644 --- a/sdk/parity/symbol-map.json +++ b/sdk/parity/symbol-map.json @@ -1,6 +1,29 @@ { "_comment": "Symbol-level Go->Python->TS API-surface parity map. Go is the oracle (sdk/go/{helpers,resolvers,core,connect,connectserver,profiles/music,helperstest}). One entry per Go public symbol (pkg-qualified). 'python'/'ts' hold the mapped public name or null when that language exposes no public symbol of that name. allowlist_reason marks a deliberate documented divergence (shrink-only). Enforced by sdk/python/tests/test_api_surface_parity.py. TS object factories use the create* prefix and value generators use generate* (Go NewX stays idiomatic); the guarded-client names (guarded_client/guarded_async_client vs guardedFetchFromEnv) are a documented idiomatic divergence, not a transliteration.", "go_exclusions": { + "helpers.DelegationOversized": "Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens.", + "helpers.DelegationMalformed": "Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens.", + "helpers.DelegationTooDeep": "Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens.", + "helpers.DelegationBadAlg": "Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens.", + "helpers.DelegationBadTyp": "Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens.", + "helpers.DelegationCritPresent": "Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens.", + "helpers.DelegationMissingClaim": "Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens.", + "helpers.DelegationBadClaim": "Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens.", + "helpers.DelegationUnknownClaim": "Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens.", + "helpers.DelegationUnsupportedRestriction": "Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens.", + "helpers.DelegationAdvisoryShadowsClaim": "Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens.", + "helpers.DelegationUntrustedIssuer": "Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens.", + "helpers.DelegationBadSignature": "Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens.", + "helpers.DelegationBadJWK": "Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens.", + "helpers.DelegationJWKMismatch": "Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens.", + "helpers.DelegationChildIssuerMismatch": "Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens.", + "helpers.DelegationExpired": "Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens.", + "helpers.DelegationNotYetValid": "Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens.", + "helpers.DelegationExpiryWidened": "Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens.", + "helpers.DelegationAudienceMismatch": "Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens.", + "helpers.DelegationAudienceWidened": "Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens.", + "helpers.DelegationScopeWidened": "Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens.", + "helpers.DelegationHolderMismatch": "Member of the DelegationFailure token set of the Go-only delegation verifier (see the delegation verifier DECISION); delegation-chains.json pins the tokens.", "connect.CallMalformed": "Member of the mapped connect.CallErrorKind enum. Go declares each as a package-level constant; Python spells it CallErrorKind. and TypeScript as a member of the CallErrorKind literal union, so neither is a top-level export the surface enumerator can see. The TYPE carries the parity claim and the shared corpora pin the values.", "connect.CallNotSent": "Member of the mapped connect.CallErrorKind enum. Go declares each as a package-level constant; Python spells it CallErrorKind. and TypeScript as a member of the CallErrorKind literal union, so neither is a top-level export the surface enumerator can see. The TYPE carries the parity claim and the shared corpora pin the values.", "connect.CallNotSignable": "Member of the mapped connect.CallErrorKind enum. Go declares each as a package-level constant; Python spells it CallErrorKind. and TypeScript as a member of the CallErrorKind literal union, so neither is a top-level export the surface enumerator can see. The TYPE carries the parity claim and the shared corpora pin the values.", @@ -545,6 +568,78 @@ "python": "DEFAULT_KEY_VALIDITY", "ts": "DEFAULT_KEY_VALIDITY_MS" }, + "helpers.DelegationClockLeeway": { + "allowlist_reason": "Go-only delegation chain verifier \u2014 OPEN DECISION in docs/sdk-parity-matrix.md: the holder-bound JWT profile ships with one verifier, in Go, that the Exchange imports; the TypeScript and Python verifiers are deferred and tracked as a FORA-84 follow-up, and the chain cases in sdk/go/helpers/testdata/delegation-chains.json become a tri-replayed *-vectors.json corpus when they land. The mirrors a case carries (the plaintext Delegation fields next to the token) are replayed only by an Exchange's own tests; nothing in this repository reads them", + "decision_anchor": "delegation verifier", + "python": null, + "ts": null + }, + "helpers.DelegationError": { + "allowlist_reason": "Go-only delegation chain verifier \u2014 OPEN DECISION in docs/sdk-parity-matrix.md: the holder-bound JWT profile ships with one verifier, in Go, that the Exchange imports; the TypeScript and Python verifiers are deferred and tracked as a FORA-84 follow-up, and the chain cases in sdk/go/helpers/testdata/delegation-chains.json become a tri-replayed *-vectors.json corpus when they land. The mirrors a case carries (the plaintext Delegation fields next to the token) are replayed only by an Exchange's own tests; nothing in this repository reads them", + "decision_anchor": "delegation verifier", + "python": null, + "ts": null + }, + "helpers.DelegationFailure": { + "allowlist_reason": "Go-only delegation chain verifier \u2014 OPEN DECISION in docs/sdk-parity-matrix.md: the holder-bound JWT profile ships with one verifier, in Go, that the Exchange imports; the TypeScript and Python verifiers are deferred and tracked as a FORA-84 follow-up, and the chain cases in sdk/go/helpers/testdata/delegation-chains.json become a tri-replayed *-vectors.json corpus when they land. The mirrors a case carries (the plaintext Delegation fields next to the token) are replayed only by an Exchange's own tests; nothing in this repository reads them", + "decision_anchor": "delegation verifier", + "python": null, + "ts": null + }, + "helpers.DelegationInput": { + "allowlist_reason": "Go-only delegation chain verifier \u2014 OPEN DECISION in docs/sdk-parity-matrix.md: the holder-bound JWT profile ships with one verifier, in Go, that the Exchange imports; the TypeScript and Python verifiers are deferred and tracked as a FORA-84 follow-up, and the chain cases in sdk/go/helpers/testdata/delegation-chains.json become a tri-replayed *-vectors.json corpus when they land. The mirrors a case carries (the plaintext Delegation fields next to the token) are replayed only by an Exchange's own tests; nothing in this repository reads them", + "decision_anchor": "delegation verifier", + "python": null, + "ts": null + }, + "helpers.DelegationTokenType": { + "allowlist_reason": "Go-only delegation chain verifier \u2014 OPEN DECISION in docs/sdk-parity-matrix.md: the holder-bound JWT profile ships with one verifier, in Go, that the Exchange imports; the TypeScript and Python verifiers are deferred and tracked as a FORA-84 follow-up, and the chain cases in sdk/go/helpers/testdata/delegation-chains.json become a tri-replayed *-vectors.json corpus when they land. The mirrors a case carries (the plaintext Delegation fields next to the token) are replayed only by an Exchange's own tests; nothing in this repository reads them", + "decision_anchor": "delegation verifier", + "python": null, + "ts": null + }, + "helpers.ErrDelegationInvalid": { + "allowlist_reason": "Go-only delegation chain verifier \u2014 OPEN DECISION in docs/sdk-parity-matrix.md: the holder-bound JWT profile ships with one verifier, in Go, that the Exchange imports; the TypeScript and Python verifiers are deferred and tracked as a FORA-84 follow-up, and the chain cases in sdk/go/helpers/testdata/delegation-chains.json become a tri-replayed *-vectors.json corpus when they land. The mirrors a case carries (the plaintext Delegation fields next to the token) are replayed only by an Exchange's own tests; nothing in this repository reads them", + "decision_anchor": "delegation verifier", + "python": null, + "ts": null + }, + "helpers.MaxDelegationChainDepth": { + "allowlist_reason": "Go-only delegation chain verifier \u2014 OPEN DECISION in docs/sdk-parity-matrix.md: the holder-bound JWT profile ships with one verifier, in Go, that the Exchange imports; the TypeScript and Python verifiers are deferred and tracked as a FORA-84 follow-up, and the chain cases in sdk/go/helpers/testdata/delegation-chains.json become a tri-replayed *-vectors.json corpus when they land. The mirrors a case carries (the plaintext Delegation fields next to the token) are replayed only by an Exchange's own tests; nothing in this repository reads them", + "decision_anchor": "delegation verifier", + "python": null, + "ts": null + }, + "helpers.MaxDelegationNumericDate": { + "allowlist_reason": "Go-only delegation chain verifier \u2014 OPEN DECISION in docs/sdk-parity-matrix.md: the holder-bound JWT profile ships with one verifier, in Go, that the Exchange imports; the TypeScript and Python verifiers are deferred and tracked as a FORA-84 follow-up, and the chain cases in sdk/go/helpers/testdata/delegation-chains.json become a tri-replayed *-vectors.json corpus when they land. The mirrors a case carries (the plaintext Delegation fields next to the token) are replayed only by an Exchange's own tests; nothing in this repository reads them", + "decision_anchor": "delegation verifier", + "python": null, + "ts": null + }, + "helpers.MaxDelegationTokenBytes": { + "allowlist_reason": "Go-only delegation chain verifier \u2014 OPEN DECISION in docs/sdk-parity-matrix.md: the holder-bound JWT profile ships with one verifier, in Go, that the Exchange imports; the TypeScript and Python verifiers are deferred and tracked as a FORA-84 follow-up, and the chain cases in sdk/go/helpers/testdata/delegation-chains.json become a tri-replayed *-vectors.json corpus when they land. The mirrors a case carries (the plaintext Delegation fields next to the token) are replayed only by an Exchange's own tests; nothing in this repository reads them", + "decision_anchor": "delegation verifier", + "python": null, + "ts": null + }, + "helpers.TrustedIssuer": { + "allowlist_reason": "Go-only delegation chain verifier \u2014 OPEN DECISION in docs/sdk-parity-matrix.md: the holder-bound JWT profile ships with one verifier, in Go, that the Exchange imports; the TypeScript and Python verifiers are deferred and tracked as a FORA-84 follow-up, and the chain cases in sdk/go/helpers/testdata/delegation-chains.json become a tri-replayed *-vectors.json corpus when they land. The mirrors a case carries (the plaintext Delegation fields next to the token) are replayed only by an Exchange's own tests; nothing in this repository reads them", + "decision_anchor": "delegation verifier", + "python": null, + "ts": null + }, + "helpers.VerifiedDelegation": { + "allowlist_reason": "Go-only delegation chain verifier \u2014 OPEN DECISION in docs/sdk-parity-matrix.md: the holder-bound JWT profile ships with one verifier, in Go, that the Exchange imports; the TypeScript and Python verifiers are deferred and tracked as a FORA-84 follow-up, and the chain cases in sdk/go/helpers/testdata/delegation-chains.json become a tri-replayed *-vectors.json corpus when they land. The mirrors a case carries (the plaintext Delegation fields next to the token) are replayed only by an Exchange's own tests; nothing in this repository reads them", + "decision_anchor": "delegation verifier", + "python": null, + "ts": null + }, + "helpers.VerifyDelegation": { + "allowlist_reason": "Go-only delegation chain verifier \u2014 OPEN DECISION in docs/sdk-parity-matrix.md: the holder-bound JWT profile ships with one verifier, in Go, that the Exchange imports; the TypeScript and Python verifiers are deferred and tracked as a FORA-84 follow-up, and the chain cases in sdk/go/helpers/testdata/delegation-chains.json become a tri-replayed *-vectors.json corpus when they land. The mirrors a case carries (the plaintext Delegation fields next to the token) are replayed only by an Exchange's own tests; nothing in this repository reads them", + "decision_anchor": "delegation verifier", + "python": null, + "ts": null + }, "helpers.DirectoryDocument": { "allowlist_reason": null, "python": "directory_document", @@ -850,6 +945,16 @@ "python": "SchemaVerdict", "ts": "SchemaVerdict" }, + "helpers.ScopeCovers": { + "allowlist_reason": null, + "python": "scope_covers", + "ts": "scopeCovers" + }, + "helpers.ScopesCover": { + "allowlist_reason": null, + "python": "scopes_cover", + "ts": "scopesCover" + }, "helpers.ScopesSubset": { "allowlist_reason": null, "python": "scopes_subset", diff --git a/sdk/python/fora_sdk/__init__.py b/sdk/python/fora_sdk/__init__.py index a8092ba2..23912443 100644 --- a/sdk/python/fora_sdk/__init__.py +++ b/sdk/python/fora_sdk/__init__.py @@ -185,7 +185,7 @@ WellKnownKeyResolver, wba_directory_url, ) -from .scopes import apply_scopes, normalize_scopes, scopes_subset +from .scopes import apply_scopes, normalize_scopes, scope_covers, scopes_cover, scopes_subset from .signedurl import sign_ed25519_signed_url, verify_ed25519_signed_url from .signing_transport import OutboundRequest, SignedOutbound, SignerSource, SigningTransport from .strict import StrictViolationError, check_strict @@ -383,6 +383,8 @@ "registration_failure_detail", "request_auth_failure_detail", "retrieval_auth_failure_detail", + "scope_covers", + "scopes_cover", "scopes_subset", "sign_agent_binding", "sign_ed25519_signed_url", diff --git a/sdk/python/fora_sdk/scopes.py b/sdk/python/fora_sdk/scopes.py index 9ca920c6..59de0b78 100644 --- a/sdk/python/fora_sdk/scopes.py +++ b/sdk/python/fora_sdk/scopes.py @@ -2,7 +2,9 @@ (helpers/scopes.go). The subscriptions/entitlements a requester holds are a SUPPLIED credential: the application hands the SDK what it holds and the SDK plumbs it into the request. ``normalize_scopes``/``scopes_subset`` are pure, -byte-deterministic string ops pinned to the shared scopes-vectors.json. +byte-deterministic string ops pinned to the shared scopes-vectors.json; +``scope_covers``/``scopes_cover`` implement the protocol's segment-wise +scope-matching rule and are pinned to scope-match-vectors.json. """ from __future__ import annotations @@ -27,12 +29,42 @@ def normalize_scopes(scopes: list[str]) -> list[str]: return out +def scope_covers(granted: str, required: str) -> bool: + """Report whether one granted scope covers one required scope under the + protocol's scope-matching rule (stated on Requester.scopes): split both on + ``:``; each granted segment equals the required segment or is ``*``; a + ``*`` in the last position of the grant covers every remaining required + segment and there must be at least one; no implicit prefix match; a grant + with more segments than the requirement never covers it; a ``*`` inside the + requirement is a literal segment; an empty string covers nothing and is + covered by nothing. Case-sensitive, no trimming. + """ + if granted == "" or required == "": + return False + g = granted.split(":") + r = required.split(":") + for i, seg in enumerate(g): + if seg == "*" and i == len(g) - 1: + return i < len(r) + if i >= len(r) or (seg != "*" and seg != r[i]): + return False + return len(g) == len(r) + + +def scopes_cover(granted: list[str], required: list[str]) -> bool: + """Report whether every scope in ``required`` is covered (``scope_covers``) + by at least one scope in ``granted``. An empty ``required`` is always covered. + """ + return all(any(scope_covers(g, req) for g in granted) for req in required) + + def scopes_subset(sub: list[str], superset: list[str]) -> bool: - """Report whether every scope in ``sub`` is present in ``superset`` — the - delegation-attenuation rule (Delegation.scopes MUST be a subset of the - principal's granted scopes). An empty ``sub`` is always a subset. + """Report whether every scope in ``sub`` is covered by some scope in + ``superset`` under ``scope_covers`` (grant containment, not exact set + membership) — the delegation-attenuation rule. A child may narrow a parent + wildcard but may not widen one. Same check as ``scopes_cover(superset, sub)``. """ - return set(sub).issubset(superset) + return scopes_cover(superset, sub) def apply_scopes(scopes: list[str]) -> list[str]: diff --git a/sdk/python/tests/test_api_surface_parity.py b/sdk/python/tests/test_api_surface_parity.py index c9c487b7..2449d535 100644 --- a/sdk/python/tests/test_api_surface_parity.py +++ b/sdk/python/tests/test_api_surface_parity.py @@ -120,7 +120,15 @@ # client's Go factory, folds into the Python AdminClient class constructor exactly as # every other NewX does, and carries that same recorded reason. One entry, one # already-recorded class. -BASELINE_ALLOWLIST = 17 +# +# Then 17 -> 29, a NEW decision class: the holder-bound JWT delegation profile ships with +# one verifier, in Go (helpers.VerifyDelegation and its eleven input, result, error and +# profile-constant symbols), which the Exchange imports; the TypeScript and Python +# verifiers are deferred and tracked as a FORA-84 follow-up. All twelve entries carry the +# "delegation verifier" decision_anchor, so the gap is a DECISION bullet in the matrix, +# never a silent null. The failure-token constants sit in go_exclusions like the PoP +# tokens. Shrinks back by twelve when the two ports land. +BASELINE_ALLOWLIST = 29 # HARD ZERO (was a shrink-only ratchet at 19) — undocumented TS-present / Python-null # gaps. The PRESENCE check skips nulls, so absent this ceiling a NEW Python-null gap diff --git a/sdk/python/tests/test_parity_corpora_nonempty.py b/sdk/python/tests/test_parity_corpora_nonempty.py index 780316ba..23686b0c 100644 --- a/sdk/python/tests/test_parity_corpora_nonempty.py +++ b/sdk/python/tests/test_parity_corpora_nonempty.py @@ -78,6 +78,8 @@ def _whole(doc: Any) -> Any: (GO_TESTDATA / "signedurl-vectors.json", _whole, "signedurl"), (GO_TESTDATA / "scopes-vectors.json", lambda d: d["normalize"], "scopes-normalize"), (GO_TESTDATA / "scopes-vectors.json", lambda d: d["subset"], "scopes-subset"), + (GO_TESTDATA / "scope-match-vectors.json", lambda d: d["covers"], "scope-match-covers"), + (GO_TESTDATA / "scope-match-vectors.json", lambda d: d["cover_sets"], "scope-match-cover-sets"), (GO_TESTDATA / "audience-vectors.json", lambda d: d["bare_domain"], "bare-domain"), (GO_TESTDATA / "audience-vectors.json", lambda d: d["audience"], "audience"), (GO_TESTDATA / "discovery-hint-vectors.json", lambda d: d["parse"], "discovery-hint-parse"), diff --git a/sdk/python/tests/test_scope_match_parity.py b/sdk/python/tests/test_scope_match_parity.py new file mode 100644 index 00000000..d58440d1 --- /dev/null +++ b/sdk/python/tests/test_scope_match_parity.py @@ -0,0 +1,37 @@ +"""Scope-matching parity (Python side). + +Mirrors the sdk/ts sibling sdk/ts/tests/scope-match.parity.test.ts. + +``fora_sdk.scopes.scope_covers``/``scopes_cover``/``scopes_subset`` MUST give the +verdicts in sdk/go/helpers/testdata/scope-match-vectors.json, which is written +by hand from the protocol's segment-wise scope-matching rule (Requester.scopes), +not emitted by an implementation: + - covers: {granted:str, required:str, expected:bool} + - cover_sets: {granted:[str], required:[str], expected:bool}; the same row + also pins scopes_subset(required, granted). +""" + +from __future__ import annotations + +import pytest + +from conftest import GO_TESTDATA, load_json + +from fora_sdk.scopes import scope_covers, scopes_cover, scopes_subset + +_VECTORS = load_json(GO_TESTDATA / "scope-match-vectors.json") +_COVERS = _VECTORS["covers"] +_COVER_SETS = _VECTORS["cover_sets"] + + +@pytest.mark.parametrize("vector", _COVERS, ids=[v["name"] for v in _COVERS]) +def test_scope_covers_matches_rule(vector: dict) -> None: + assert scope_covers(vector["granted"], vector["required"]) is vector["expected"] + + +@pytest.mark.parametrize("vector", _COVER_SETS, ids=[v["name"] for v in _COVER_SETS]) +def test_scopes_cover_matches_rule(vector: dict) -> None: + granted = list(vector["granted"]) + required = list(vector["required"]) + assert scopes_cover(granted, required) is vector["expected"] + assert scopes_subset(required, granted) is vector["expected"] diff --git a/sdk/ts/src/scopes.ts b/sdk/ts/src/scopes.ts index d6103269..04353121 100644 --- a/sdk/ts/src/scopes.ts +++ b/sdk/ts/src/scopes.ts @@ -2,7 +2,9 @@ // (helpers/scopes.go). The subscriptions/entitlements a requester holds are a // SUPPLIED credential: the application hands the SDK what it holds and the SDK // plumbs it into the request. NormalizeScopes/ScopesSubset are pure, byte- -// deterministic string ops pinned to the shared scopes-vectors.json. +// deterministic string ops pinned to the shared scopes-vectors.json; +// scopeCovers/scopesCover implement the protocol's segment-wise scope-matching +// rule and are pinned to scope-match-vectors.json. /** * normalizeScopes returns scopes with empty entries dropped, duplicates removed @@ -24,16 +26,44 @@ export function normalizeScopes(scopes: string[]): string[] { } /** - * scopesSubset reports whether every scope in `sub` is present in `sup`. It is - * the delegation-attenuation rule (Delegation.scopes MUST be a subset of the - * principal's granted scopes). An empty `sub` is always a subset. + * scopeCovers reports whether one granted scope covers one required scope under + * the protocol's scope-matching rule (stated on Requester.scopes): split both + * on ":"; each granted segment equals the required segment or is "*"; a "*" in + * the last position of the grant covers every remaining required segment and + * there must be at least one; no implicit prefix match; a grant with more + * segments than the requirement never covers it; a "*" inside the requirement + * is a literal segment; an empty string covers nothing and is covered by + * nothing. Case-sensitive, no trimming. */ -export function scopesSubset(sub: string[], sup: string[]): boolean { - const set = new Set(sup); - for (const s of sub) { - if (!set.has(s)) return false; +export function scopeCovers(granted: string, required: string): boolean { + if (granted === "" || required === "") return false; + const g = granted.split(":"); + const r = required.split(":"); + for (let i = 0; i < g.length; i++) { + const seg = g[i]; + if (seg === "*" && i === g.length - 1) return i < r.length; + if (i >= r.length || (seg !== "*" && seg !== r[i])) return false; } - return true; + return g.length === r.length; +} + +/** + * scopesCover reports whether every scope in `required` is covered + * (scopeCovers) by at least one scope in `granted`. An empty `required` is + * always covered. + */ +export function scopesCover(granted: string[], required: string[]): boolean { + return required.every((req) => granted.some((g) => scopeCovers(g, req))); +} + +/** + * scopesSubset reports whether every scope in `sub` is covered by some scope in + * `sup` under scopeCovers (grant containment, not exact set membership). It is + * the delegation-attenuation rule: a child may narrow a parent wildcard but may + * not widen one. Same check as scopesCover(sup, sub). + */ +export function scopesSubset(sub: string[], sup: string[]): boolean { + return scopesCover(sup, sub); } /** diff --git a/sdk/ts/tests/scope-match.parity.test.ts b/sdk/ts/tests/scope-match.parity.test.ts new file mode 100644 index 00000000..a2a98a6f --- /dev/null +++ b/sdk/ts/tests/scope-match.parity.test.ts @@ -0,0 +1,41 @@ +// Scope-matching parity (TypeScript side). +// +// sdk/ts scopeCovers/scopesCover/scopesSubset MUST give the verdicts in +// sdk/go/helpers/testdata/scope-match-vectors.json, which is written by hand +// from the protocol's segment-wise scope-matching rule (Requester.scopes), not +// emitted by an implementation: +// - covers: {granted:string, required:string, expected:boolean} +// - cover_sets: {granted:string[], required:string[], expected:boolean}; +// the same row also pins scopesSubset(required, granted). +import { describe, it, expect } from "vitest"; +import { scopeCovers, scopesCover, scopesSubset } from "../src/scopes.ts"; +import vectorsFile from "../../go/helpers/testdata/scope-match-vectors.json"; + +type CoverVector = { name: string; granted: string; required: string; expected: boolean }; +type CoverSetVector = { name: string; granted: string[]; required: string[]; expected: boolean }; +type ScopeMatchVectorsFile = { covers: CoverVector[]; cover_sets: CoverSetVector[] }; + +const vectors = vectorsFile as ScopeMatchVectorsFile; + +describe("sdk/ts scopeCovers matches the written scope-matching rule", () => { + it("covers vector set is non-empty", () => { + expect(vectors.covers.length).toBeGreaterThan(0); + }); + for (const v of vectors.covers) { + it(`scopeCovers(${v.name}) === ${v.expected}`, () => { + expect(scopeCovers(v.granted, v.required)).toBe(v.expected); + }); + } +}); + +describe("sdk/ts scopesCover and scopesSubset match the written rule", () => { + it("cover_sets vector set is non-empty", () => { + expect(vectors.cover_sets.length).toBeGreaterThan(0); + }); + for (const v of vectors.cover_sets) { + it(`scopesCover(${v.name}) === ${v.expected}`, () => { + expect(scopesCover(v.granted, v.required)).toBe(v.expected); + expect(scopesSubset(v.required, v.granted)).toBe(v.expected); + }); + } +}); diff --git a/website/src/content/docs/architecture/production-architecture.mdx b/website/src/content/docs/architecture/production-architecture.mdx index ad0ec0ea..4a28cfb0 100644 --- a/website/src/content/docs/architecture/production-architecture.mdx +++ b/website/src/content/docs/architecture/production-architecture.mdx @@ -61,7 +61,7 @@ Every party in the protocol authenticates via Ed25519 key pairs. Each party anno **Verification at Exchange:** - **Requester signature** — Exchange verifies the RFC 9421 HTTP Message Signature on the request (carried in HTTP headers, not a message field) against the requester's published Ed25519 key in its WBA directory (the JWK Set at `/.well-known/http-message-signatures-directory`), the key identified by its RFC 7638 thumbprint (the RFC 9421 `keyid`) - **Forwarding** — on a request a Broker sent, the Exchange verifies the Broker's own RFC 9421 HTTP Message Signature and, on a purchase, the agent's `AgentAcceptance` in the body against the agent's registered key (see [Multi-hop forwarding](/protocol/authentication/#multi-hop-forwarding)) -- **Delegation JWT** — when a `Delegation` token is attached, Exchange verifies the authority JWT signature against the principal's published Ed25519 key (`principal_domain`) +- **Delegation chain** — when a `Delegation` token is attached, Exchange verifies the chain with the issuer key configured on the subscription record (never a fetched key), checks that the holder key is the request signer, and reads the effective scopes and expiry from it ([Delegation](/protocol/authentication/#delegation)) - **Scope-based term selection** — Exchange presents the terms the requester's stated `scopes` cover. Terms outside them are not returned (the requester never learns they exist). The scopes are the requester's statement, not a verified grant; an Exchange may require a verified delegation before it presents a scoped term ### Supported Transaction Patterns diff --git a/website/src/content/docs/components/broker/overview.mdx b/website/src/content/docs/components/broker/overview.mdx index d2d0ea34..ea2bdb3a 100644 --- a/website/src/content/docs/components/broker/overview.mdx +++ b/website/src/content/docs/components/broker/overview.mdx @@ -195,13 +195,13 @@ The Broker queries its Exchanges as it would without the profile, and each query ### Scope Forwarding -The Broker forwards `Requester.scopes` to every Exchange it queries. Scopes decide which terms each Exchange presents; they are the agent's statement, which the Broker passes on under its own signature, and the Exchange does not verify them. The Broker does not modify scopes; it passes them through as-is. +The Broker forwards `Requester.scopes` to every Exchange it queries. Scopes decide which non-gated terms each Exchange presents; they are the agent's statement, which the Broker passes on under its own signature, and the Exchange does not verify them. The Broker does not modify scopes; it passes them through as-is. For a term an Exchange gates behind a verified delegation, the stated scopes count for nothing: the Exchange reads scopes from the verified delegation only, and since it honours no delegation on a query the Broker originated, a gated term stays hidden on that query. ### Delegation Pass-Through The Broker passes `Requester.delegation` through unchanged, and the **agent remains the holder**. A Broker is never delegated to. On a purchase, the Exchange matches the delegation's `cnf.jkt` against the key that signed the agent's `AgentAcceptance`, so the delegation stays bound to the agent's private key through the Broker, exactly as on a direct call. On a query the Broker originates, the Broker signs alone, so nothing can pass the delegation's holder binding: the Exchange does not honour it and answers as if none were present. Through a Broker, the delegation counts only on a purchase, through the agent's acceptances. How an agent proves a holder-of-key entitlement at discovery time through a Broker is an open protocol question. An Exchange that requires a verified delegation before it presents a scoped term presents no such term on the Broker's query. -The Broker cannot narrow a delegation: only the key named in the chain's `cnf` can issue a valid child JWT, and that is the agent's key, not the Broker's. Narrowing is done by a holder in the chain, as [Delegation](/protocol/authentication/#verification) describes. +The Broker cannot narrow a delegation: only the key named in the chain's `cnf` can issue a valid child token, and that is the agent's key, not the Broker's. Narrowing is done by a holder in the chain, as [Delegation](/protocol/authentication/#delegation) describes: the child carries its complete parent in `fora_parent`, so the Broker also cannot swap a parent, and the Exchange verifies the root with a key it has configured, so the Broker cannot introduce one. ### SubscriptionQuotaInfo Aggregation diff --git a/website/src/content/docs/components/exchange/request-flows.mdx b/website/src/content/docs/components/exchange/request-flows.mdx index 22045649..cce383df 100644 --- a/website/src/content/docs/components/exchange/request-flows.mdx +++ b/website/src/content/docs/components/exchange/request-flows.mdx @@ -111,23 +111,7 @@ func (h *ExchangeHandler) DiscoverResources( forav1.RequestAuthFailureReason_REQUEST_AUTH_FAILURE_REASON_SIGNATURE_INVALID)) } - // 2c. Verify delegation (if present) on a DIRECT query. - // Verifies the delegation JWT chain (chain linkage + cnf holder binding) - // against the principal's published key. On a query a Broker originated - // the Broker signs alone, so nothing can pass the holder binding: the - // delegation is not honoured, and the query is answered as if none were - // present. - delegation := requester.GetDelegation() - if brokerLed { - delegation = nil - } - if delegation != nil { - if err := h.verifyDelegationJWT(ctx, requester); err != nil { - return nil, connect.NewError(connect.CodePermissionDenied, err) - } - } - - // 2d. On a DIRECT query, resolve the agent's registered account from the + // 2c. On a DIRECT query, resolve the agent's registered account from the // directory the signature resolved from (verified.SignatureAgent; see // "Accounts" on the authentication page). Every currently valid key that // directory publishes acts for the account, so the key that verified @@ -149,24 +133,55 @@ func (h *ExchangeHandler) DiscoverResources( return nil, connect.NewError(connect.CodeNotFound, err) } + // 3b. Verify the delegation (if present) on a DIRECT query, once per + // subscription record. Trust is per record: the SDK verifier is given + // the keys that record lists, so an issuer trusted for one subscription + // grants nothing on another. A record whose keys refuse the chain keeps + // its gated terms hidden; the refusal is journaled, never reported. The + // holder is the RFC 9421 signer's key from the HTTP proof of step 2, + // which stays separate from the delegation result. On a query a Broker + // originated the Broker signs alone, so nothing can pass the holder + // binding: the delegation is not honoured, and the query is answered + // as if none were present. A token over the size limit never gets + // here: step 1 refused the request. + grants := map[string]*helpers.VerifiedDelegation{} // subscription record ID -> verified grant + if direct && requester.GetDelegation() != nil { + holder, err := helpers.Thumbprint(verified.PublicKey) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, err) + } + for _, record := range h.subscriptions.RecordsForTenant(ctx, tenantCfg.ID) { + grant, err := helpers.VerifyDelegation(helpers.DelegationInput{ + Token: requester.GetDelegation().GetToken(), + TrustedIssuers: record.TrustedIssuers, + Audience: tenantCfg.Identity, + Holder: holder, + Now: h.clock.Now(), + }) + if err != nil { + h.journal.DelegationRefused(ctx, record.ID, err) + continue + } + grants[record.ID] = grant + } + } + // 4. Application-level abuse check (optional — volumetric rate limiting is at infra layer) // The Exchange MAY return 429 for application-level abuse, e.g., query-to-transaction // ratio too low (agent discovering but never buying). Volumetric rate limiting (per-IP, // per-tenant RPS caps) belongs in HAProxy/Envoy/API Gateway in front of this service. // 5. Catalog lookup (lock-free read, scope-filtered) - // The catalog is filtered by requester.scopes, the requester's own - // statement — terms outside the stated scopes are not returned. When a - // direct query carries a verified delegation, this Exchange also requires - // its grant to cover a term's scopes (a narrower use of the MAY in - // LicenseTerm.scopes): the effective scopes are the intersection of - // requester.scopes and the delegation's granted scopes. A query a Broker - // originated honours no delegation (2c), so its stated scopes alone decide. - effectiveScopes := requester.Scopes - if delegation != nil { - effectiveScopes = intersectScopes(requester.Scopes, delegation.Scopes) - } - offers, err := h.catalog.FindOffers(ctx, tenantCfg.ID, req.Msg.Uris, effectiveScopes) + // A non-gated term is filtered by requester.scopes, the requester's own + // statement — terms outside the stated scopes are not returned. A gated + // term (a subscription term) is presented only when the grant verified + // under the term's own subscription record covers the term's scopes, by + // the one matching rule on Requester.scopes; requester.scopes is ignored + // for it, and a record with no grant — every record on a query a Broker + // originated (3b) — keeps its gated terms hidden, as if they did not + // exist. The catalog bounds each gated offer's expires_at by its grant's + // ExpiresAt, so the offer never outlives the chain. + offers, err := h.catalog.FindOffers(ctx, tenantCfg.ID, req.Msg.Uris, requester.Scopes, grants) if err != nil { return nil, connect.NewError(connect.CodeInternal, err) } @@ -196,17 +211,17 @@ func (h *ExchangeHandler) DiscoverResources( subOffer := proto.Clone(offer).(*forav1.Offer) // A subscription variant is a distinct offer, so it gets a fresh // opaque offer_id — never one derived from the base offer's id. - subOffer.OfferId = "offer-" + ulid.Make().String() + subOffer.OfferId = "offer-" + newOpaqueID() subOffer.SubscriptionId = proto.String(subStatus.SubscriptionID) subOffer.Pricing = &forav1.Pricing{ - Model: offer.Pricing.Model, - Rate: 0, // zero marginal cost under subscription - Currency: offer.Pricing.Currency, - UnitCost: proto.Float64(0), + Model: offer.Pricing.Model, + Rate: "0", // zero marginal cost under subscription; money is a decimal string + Currency: offer.Pricing.Currency, + UnitCost: proto.String("0"), EstimatedQuantity: offer.Pricing.EstimatedQuantity, } // Populate SubscriptionQuotaInfo for proactive quota signaling - subOffer.SubscriptionQuota = h.billing.GetSubscriptionQuota( + subOffer.SubscriptionQuota = h.billing.SubscriptionQuota( ctx, subStatus.SubscriptionID, tenantCfg.ID, ) subOffers = append(subOffers, subOffer) @@ -338,17 +353,12 @@ sigs := helpers.AllSignaturesFromContext(ctx) ### Delegation Verification -When `Requester.delegation` is present, the Exchange verifies the delegation JWT chain before honouring it: - -1. Decode the base64url-encoded `token` (JWS) from the `Delegation` message -2. Verify the Ed25519 signature chain — the authority JWT signed by `principal_domain`'s published key, each child JWT signed by the key its parent named in `cnf` (chain-linkage invariant) -3. Verify holder binding — the key that signed the RFC 9421 request MUST hash to the final JWT's `cnf.jkt`. On a purchase sub-request a Broker re-packaged, the request signer is the Broker, so compare the key each item's `AgentAcceptance` verifies under instead. On a query a Broker originated nothing can pass this check, so the delegation is not verified or honoured there, and the query is answered as if none were present -4. Evaluate constraints (scope coverage, spend caps, expiration) — the grant covers scopes only if each child's `scope` is contained in its parent's -5. Extract effective scopes (intersection of requester scopes and delegation scopes) -6. Check `expires_at` against current time -7. Optionally check `revocation_uri` for high-value transactions +When `Requester.delegation` is present, the Exchange runs the chain through the profile's verifier before honoring it. The procedure, the limits and the outcome of each failure are stated once, in [Verification](/protocol/authentication/#verification). What the handler supplies and does: -See [Authentication](/protocol/authentication/) for the full delegation model. +1. The trusted root keys come from the subscription record that owns the gated term, never from `principal_domain` or a fetched directory +2. The holder is the key the request proved: the RFC 9421 request signer on a direct call, the key each item's `AgentAcceptance` verifies under on a purchase a Broker relayed. On a query a Broker originated nothing can pass this check, so the delegation is not verified or honored there, and the query is answered as if none were present +3. For a gated term, the effective scopes the verifier returns alone decide; `Requester.scopes` is ignored +4. A gated offer expires no later than the effective expiry the verifier returns ### Provider Trust Verification diff --git a/website/src/content/docs/getting-started/enterprise.mdx b/website/src/content/docs/getting-started/enterprise.mdx index 443dd4f3..4896f28b 100644 --- a/website/src/content/docs/getting-started/enterprise.mdx +++ b/website/src/content/docs/getting-started/enterprise.mdx @@ -64,10 +64,10 @@ Enterprise IAM maps user roles to FORA scopes. The Broker states them on each qu 1. User authenticates via SSO (SAML/OIDC to enterprise IdP) 2. Enterprise token service maps AD groups to FORA scopes -3. Service mints a delegation JWT signed by `iam.acme.com`'s Ed25519 key +3. Service mints a root delegation token signed by `iam.acme.com`'s Ed25519 key, in the [profile](/protocol/authentication/#delegation) the protocol defines 4. Agent carries this token in the `Delegation.token` field and presents it to the enterprise Broker with each request -5. The Broker verifies the authority JWT against IAM's published key and states the token's scopes on the queries it originates -6. The Exchange checks each term's scopes against the stated scopes. It does not honour the delegation on the Broker's query, which the Broker signs alone; on a purchase it verifies the delegation against the key that signed the agent's `AgentAcceptance` +5. The Broker verifies the root token with the IAM key it has configured, never a fetched one, and states the token's scopes on the queries it originates +6. The Exchange checks each non-gated term's scopes against the stated scopes. It does not honour the delegation on the Broker's query, which the Broker signs alone; a term it gates behind a verified delegation is hidden on that query. On a purchase it verifies the chain with the IAM key configured on the subscription record, against the key that signed the agent's `AgentAcceptance` ### Concrete Example @@ -119,20 +119,25 @@ The enterprise token service bridges your existing IAM into FORA's JWT-based aut └──────────────┘ ``` -The authority JWT contains FORA-specific claims: +The root token carries the profile's claims: ```json -// Authority JWT claims (signed by iam.acme.com Ed25519 key) +// root token — protected header +{ "alg": "Ed25519", "typ": "fora-delegation+jwt" } +// claims (signed by iam.acme.com's Ed25519 key) { "iss": "iam.acme.com", "sub": "jane@acme.com", + "aud": ["exchange.acme.internal", "exchange.dnb.example"], + "jti": "iam-2026-03-20-7c1e", "scope": "tax:customer:read legal:read", - "fora_max_spend_cents": 100000, // $1,000 department budget "exp": 1774375200, // 2026-03-20T18:00:00Z, 2-hour session "cnf": { "jkt": "" } } ``` +The department budget is not a token claim: the Broker tracks it by `budget_scope`, and a chain that carries a cap claim is refused by the protocol's verifier of this release. + The `Delegation` message carries this token on the wire: ```protobuf @@ -141,8 +146,7 @@ Delegation { principal_id: "jane@acme.com" scopes: ["tax:customer:read", "legal:read"] expires_at: "2026-03-20T18:00:00Z" - max_spend_cents: 100000 // $1,000 in currency minor units - token: "eyJhbGciOiJFZERTQSJ9.." + token: "" token_format: "jwt" } ``` @@ -151,12 +155,12 @@ Key properties of this flow: - **No FORA-specific password.** Users authenticate through existing SSO. The token service is the only component that needs FORA awareness. - **Scopes derived from roles.** A single mapping table (`AD group → FORA scopes`) governs all access. Update the mapping, and every future token reflects the change. -- **Time-bounded.** Tokens expire with the SSO session. A leaked token is useless after expiry. -- **Spend-capped.** `max_spend_cents` prevents runaway costs. The Exchange tracks cumulative spend against this cap. +- **Time-bounded.** Tokens expire with the SSO session. A leaked token is useless after expiry, and useless before it too without the agent's private key, because the grant is bound to that key. +- **Budget-capped at the Broker.** The Broker tracks cumulative spend per `budget_scope`, so runaway cost is stopped at the chokepoint every request passes. ## Agent Delegation Within the Enterprise -Delegation JWTs support offline narrowing — an agent can narrow a token's permissions by issuing a child delegation JWT without calling back to IAM. This enables safe sub-agent delegation. +Delegation tokens support offline narrowing — an agent can narrow a token's permissions by issuing a child token without calling back to IAM. The child carries the complete parent in its `fora_parent` claim and the agent's public key in its header, so the chain verifies on its own. This enables safe sub-agent delegation. ### The Pattern @@ -166,26 +170,26 @@ Delegation JWTs support offline narrowing — an agent can narrow a token's perm │ Token: full scopes │ │ • tax:customer:read │ │ • legal:read │ -│ • max_spend_cents: 100000│ +│ • exp: session end │ └──────────┬──────────────┘ - │ Issue child delegation JWT (narrowing) + │ Issue child token (narrowing, parent inside fora_parent) ▼ ┌─────────────────────────┐ │ Tax Lookup Sub-Agent │ │ Token: narrowed scopes │ │ • tax:customer:read │ ← legal:read removed -│ • max_spend_cents: 20000 │ ← spend cap reduced +│ • exp: 30 minutes │ ← shorter than the parent │ • exp: +1h │ ← shorter TTL └─────────────────────────┘ ``` -The narrowed token is a new child delegation JWT linked into the chain via `cnf`. The sub-agent **cannot** widen permissions — it can only further restrict the registered grant dimensions: scope, spend (`max_spend_cents`), access count (`max_accesses`), and time (`exp`). Resource/URI access is gated through *scopes* (each requested URI is checked against the token's scopes), so confining a sub-agent to `internal://tax/` is expressed as a narrower scope, not a separate URI claim. Per-URI pattern confinement that is independent of scopes is not part of the JWT registered-claim set; deployments that need it use a vendor-namespaced claim. +The narrowed token is a new child token linked into the chain via `cnf`. The sub-agent **cannot** widen permissions — it can only further restrict the grant dimensions: scope, audience (`aud`) and time (`exp`). Resource/URI access is gated through *scopes* (each requested URI is checked against the token's scopes), so confining a sub-agent to `internal://tax/` is expressed as a narrower scope, not a separate URI claim. Per-URI pattern confinement that is independent of scopes is not part of the JWT registered-claim set; deployments that need it use a vendor-namespaced claim. This happens entirely offline. No call to IAM. No network round-trip. No central bottleneck. **Why this matters:** -- A research agent spawns 10 sub-agents for parallel data gathering. Each gets a narrowly scoped token. If one is compromised, damage is bounded by its attenuated scope and spend cap. -- A compliance agent delegates to a document retrieval agent with read-only scope and a $50 spend limit. The retrieval agent cannot access resources outside its scope or exceed its budget — the holder binding (`cnf`) and scope/spend claims are cryptographically bound to the token, so a narrowed child grant cannot be widened. +- A research agent spawns 10 sub-agents for parallel data gathering. Each gets a narrowly scoped, short-lived token. If one is compromised, damage is bounded by its attenuated scope and expiry, and by the Broker's budget for its `budget_scope`. +- A compliance agent delegates to a document retrieval agent with read-only scope. The retrieval agent cannot access resources outside its scope — the holder binding (`cnf`) and the scope claim are signed into the token, and the verifier checks every ancestor, so a narrowed child grant cannot be widened. ## Data Classification diff --git a/website/src/content/docs/getting-started/for-ai-agents.mdx b/website/src/content/docs/getting-started/for-ai-agents.mdx index 5397a76c..f65aa21e 100644 --- a/website/src/content/docs/getting-started/for-ai-agents.mdx +++ b/website/src/content/docs/getting-started/for-ai-agents.mdx @@ -186,7 +186,7 @@ When an item is denied, the Exchange returns a specific `DenialReason` so your a | Denial Reason | Meaning | What to Do | |---|---|---| | `DENIAL_REASON_QUOTA_EXCEEDED` | Subscription access count exhausted for this period | Wait for quota reset or upgrade subscription | -| `DENIAL_REASON_DELEGATION_INVALID` | Delegation missing, unverifiable (broken chain linkage or holder-binding mismatch), expired, or it widens scope beyond its parent | Obtain a fresh delegation from the principal | +| `DENIAL_REASON_DELEGATION_INVALID` | On a purchase item: your delegation is missing, or the Exchange refused the chain (malformed, chain linkage, issuer or signature, expired or not yet valid by the signed chain, addressed to another Exchange, holder binding, a child that widens its parent, or scopes or caps that do not cover the request). A token over 32768 bytes is refused as an invalid request instead, and at discovery a refused delegation hides the gated terms with no reason. See [Verification](/protocol/authentication/#verification) | Obtain a fresh delegation from the principal, bound to the key you sign with | | `DENIAL_REASON_INSUFFICIENT_BALANCE` | Requester's balance too low for this transaction | Add funds or reduce request scope | | `DENIAL_REASON_SIGNATURE_INVALID` | A signature the item depends on did not verify: the offer's, or your request acceptance, including when the items that arrived are not the set you signed | Check your signing key configuration, and that the request carries exactly the offers you signed, in order | | `DENIAL_REASON_RELAY_NOT_ACCEPTED` | The provider of this offer does not accept purchases relayed by a Broker | Buy this offer directly at its Exchange | diff --git a/website/src/content/docs/getting-started/for-exchange-operators.mdx b/website/src/content/docs/getting-started/for-exchange-operators.mdx index 6d0c9222..2bb26bc2 100644 --- a/website/src/content/docs/getting-started/for-exchange-operators.mdx +++ b/website/src/content/docs/getting-started/for-exchange-operators.mdx @@ -154,21 +154,18 @@ Cache directories and keys with a TTL and re-fetch them to pick up rotation, wit ### Delegation Verification -When a request includes a `Delegation` message (nested inside `Requester`), your Exchange verifies the JWT chain: +When a request includes a `Delegation` message (nested inside `Requester`), run the chain through the profile's verifier (`helpers.VerifyDelegation` in the Go SDK). The procedure, the limits and the checks are stated once, in [Verification](/protocol/authentication/#verification). This page adds only what your Exchange supplies to the verifier and what it does with the result: -1. Verify the JWT chain from the base64url `token` in the `Delegation` message -2. Verify the authority JWT is signed by the Ed25519 key published in the principal's WBA directory (the JWK Set at `{principal_domain}/.well-known/http-message-signatures-directory`) -3. Verify each child delegation JWT's signature (each signed by the key its parent named in `cnf`) -4. **Verify holder binding** — the key the request proves possession of MUST equal the key named in `cnf` (`cnf.jkt`): on a direct request the key that signed it (RFC 9421), on a purchase a Broker relays the key that signed each item's `AgentAcceptance`. Reject with `DENIAL_REASON_DELEGATION_INVALID` if they differ. This is mandatory: it is what makes a stolen token unusable, so a token possessed without its signing key grants nothing. On a query a Broker originated, which proves possession of no agent key, do not verify or honour the delegation: answer as if none were present. -5. Evaluate the grant's registered claims — scope restrictions, spend caps (`max_spend_cents`), access caps (`max_accesses`) — and confirm they cover the request (URI access is gated through scopes). For the JWT this means checking the registered claims along the `cnf` chain. -6. Check `expires_at` against current time — reject expired tokens with `DENIAL_REASON_DELEGATION_INVALID` -7. Optionally check `revocation_uri` for high-value transactions +1. **Trust comes from the subscription record.** Give the verifier the trusted root keys of the subscription record that owns the gated term, each with the issuer identity it belongs to. Trust is configured on that record, never fetched: not from `principal_domain`, not from `issuer`, not from a WBA directory, OIDC or JWKS. A key trusted on one record grants nothing on another. Withdraw trust by removing a key from the record's list; `revocation_uri` is not part of the profile. +2. **The holder is the key the request proved.** Give the verifier the thumbprint of that key: on a direct request the key that signed it (RFC 9421), on a purchase a Broker relays the key that signed each item's `AgentAcceptance`. The verifier refuses a mismatch. This is mandatory: it is what makes a stolen token unusable, so a token possessed without its signing key grants nothing. On a query a Broker originated, which proves possession of no agent key, do not verify or honor the delegation: answer as if none were present. +3. **One outcome per failure.** A `token` over 32768 bytes fails wire validation: the whole request is refused as invalid, before any item is read. At discovery, a chain the verifier refuses is treated as if no delegation were present: the gated terms stay hidden and no denial reason reports why; you MAY journal the refusal. At purchase, a chain the verifier refuses denies that item with `DENIAL_REASON_DELEGATION_INVALID`. +4. **Use what the verifier returns:** the root issuer, the holder thumbprint, the effective scopes and the effective expiry. A gated offer you issue MUST NOT expire later than that effective expiry. A chain that carries a cap claim (`fora_max_spend_cents`, `fora_max_accesses`, `fora_quota_period`) is refused by this release's verifier; meter subscriptions on the record, as [Subscription Quota Signaling](#subscription-quota-signaling) describes. This verification is entirely offline — no callback to the principal is required. ### Scope-Based Catalog Filtering -After verifying identity (and any delegation, as above), present the terms the requester's stated scopes cover; only those reach the `DiscoverResources` response. The scopes are the requester's own statement and you do not verify them. If a term must not be presented on a statement alone, require a verified delegation that grants its scopes, and answer a requester without one as if the term did not exist. +After verifying identity (and any delegation, as above), present the non-gated terms the requester's stated scopes cover; only those reach the `DiscoverResources` response. The scopes are the requester's own statement and you do not verify them. A gated term, a subscription term in the usual case, is different: present it only when the verified delegation's effective scopes cover it, ignore `requester.scopes` for that term, and answer a requester without a verified delegation as if the term did not exist. The matching rule is stated once, in [Scope Matching](/protocol/authentication/#scope-matching). For a resource that exists but falls outside the requester's scopes, answer exactly as for a resource you have nothing to offer for: an `OfferGroup` with no offers and `absence_reason` unset. Do not reveal that the resource exists, and do not send a reason that tells the two cases apart. This applies to enterprise RBAC and public marketplace subscriptions alike (see [Existence hiding](/protocol/authentication/#existence-hiding)). @@ -185,7 +182,7 @@ FORA defines identity- and entitlement-related denial reasons your Exchange MUST | Denial Reason | When to Return | |---|---| | `DENIAL_REASON_QUOTA_EXCEEDED` | Subscription access count exhausted for the current `quota_period` | -| `DENIAL_REASON_DELEGATION_INVALID` | Delegation token missing, unverifiable (broken chain linkage or holder-binding mismatch), expired (`expires_at` in the past), or it widens scope beyond its parent | +| `DENIAL_REASON_DELEGATION_INVALID` | On a purchase item only: the delegation is missing, or the verifier refuses the chain (malformed, chain linkage, issuer or signature, expired or not yet valid by the signed chain, another recipient, holder binding, a child that widens its parent, or scopes or caps that do not cover the request). Never at discovery, where the gated terms are hidden instead, and never for an oversized token, which wire validation refuses. See [Verification](/protocol/authentication/#verification) | And the entitlement/subscription family, for subscription-gated offers: diff --git a/website/src/content/docs/protocol/authentication.mdx b/website/src/content/docs/protocol/authentication.mdx index 6c31eff4..0a78f114 100644 --- a/website/src/content/docs/protocol/authentication.mdx +++ b/website/src/content/docs/protocol/authentication.mdx @@ -129,10 +129,12 @@ If verification fails, the Exchange rejects the request. No fallback, no degrade ## Delegation -The delegation token is a **JWT** (the default and fully-specified format) that is **holder-of-key bound**: the grant is tied to a specific key via the [RFC 7800](https://www.rfc-editor.org/rfc/rfc7800) `cnf` confirmation claim, and the holder proves possession by signing the request with that key (the same RFC 9421 signature it already sends). The token is *not* a bearer credential — copying it is useless without the matching private key. +The delegation token is a **JWT** (the only token format) that is **holder-of-key bound**: the grant is tied to a specific key via the [RFC 7800](https://www.rfc-editor.org/rfc/rfc7800) `cnf` confirmation claim, and the holder proves possession by signing the request with that key (the same RFC 9421 signature it already sends). The token is *not* a bearer credential — copying it is useless without the matching private key. > **Why not a plain bearer JWT?** A bearer JWT is usable by anyone who holds it, and it can't be narrowed for a sub-agent without a round trip to the issuer. FORA fixes both with one mechanism: `cnf` holder-of-key binding (theft-resistance) plus **chained `cnf` JWTs** (offline delegation). A principal narrows a grant for an agent by issuing a child JWT — no call back to the original issuer. +This section is the normative profile of that token. Every value a verifier needs is stated here as a number or an exact string; the Go SDK verifier and the shared test vectors are derived from this text. + ### Holder-of-key, not bearer Each delegation JWT carries `cnf.jkt` — the [RFC 7638](https://www.rfc-editor.org/rfc/rfc7638) JWK thumbprint of the key the grant is bound to. At request time the verifier computes the thumbprint of the key that signed the RFC 9421 request and checks it equals `cnf.jkt`. A thief who steals the token but not the private signing key cannot produce a valid request, so the token grants nothing. This is the same proof-of-possession mechanism as DPoP. @@ -141,13 +143,13 @@ Each delegation JWT carries `cnf.jkt` — the [RFC 7638](https://www.rfc-editor. Delegation is a chain of `cnf`-linked JWTs, content owner → principal → agent: -- **Authority JWT** — signed by the **resource owner** (the issuer). Carries the granted `scope`, `exp`, and `cnf.jkt` = the **principal's** key. The owner's public key is the verifier's only trust anchor. -- **Delegation JWT** — signed by the **principal**, narrowing the grant for a specific **agent**: `scope ⊆ parent`, shorter `exp`, `cnf.jkt` = the **agent's** key. It carries the principal's public key in its JOSE header (`jwk`) so the verifier can both link it to the authority's `cnf` and verify it. -- **Request** — signed by the **agent** (RFC 9421); the verifier checks `thumbprint(request key) == delegation cnf.jkt`. +- **Root token** — signed by the **issuer** the verifier trusts (the resource owner, or a subscription provider acting for it). Carries the granted `scope`, `exp`, and `cnf.jkt` = the **principal's** key. The issuer's configured public key is the verifier's only trust anchor. +- **Child token** — signed by the **principal**, narrowing the grant for a specific **agent**: `scope` covered by the parent's, `exp` no later than the parent's, `cnf.jkt` = the **agent's** key. It carries the principal's public key in its protected header (`jwk`) and the complete signed parent in its `fora_parent` claim, so the verifier can link it to the parent's `cnf` and verify it with no other input. +- **Request** — signed by the **agent** (RFC 9421); the verifier checks `thumbprint(request key) == cnf.jkt` of the last child. -**Chain-linkage invariant:** each JWT's signer MUST be the key the parent named in `cnf`. The owner verifies the authority; `authority.cnf` pins the principal; the delegation's header key must match that pin (and verifies the delegation); `delegation.cnf` pins the agent; the request key must match that pin. A token signed by a key not named upstream is rejected. +**Chain-linkage invariant:** each JWT's signer MUST be the key the parent named in `cnf`. The configured key verifies the root; `root.cnf` pins the principal; the child's header key must match that pin (and verifies the child); `child.cnf` pins the agent; the request key must match that pin. A token signed by a key not named upstream is rejected. -The chain narrows only — a child's `scope` must be covered by its parent's (see [Scope Matching](#scope-matching)). The principal can delegate to a sub-agent (and that sub-agent further) by issuing another child JWT, entirely offline. Verifiers need only the **owner's** public key; the principal and agent keys ride inside the chain. +The chain narrows only — a child's `scope` must be covered by its parent's (see [Scope Matching](#scope-matching)). The principal can delegate to a sub-agent (and that sub-agent further) by issuing another child JWT, entirely offline, up to the chain limit below. Verifiers need only the **issuer's** public key; the principal and agent keys ride inside the chain. ### The Delegation Message @@ -155,97 +157,202 @@ The token is carried in the `Delegation` message, nested inside `Requester`: | Field | Type | Description | |---|---|---| -| `principal_domain` | `string` | Who granted this delegation (domain for public key lookup) | +| `principal_domain` | `string` | Mirror of the root token's `iss`. Informational: trust comes from the configured key, not from this field | | `principal_id` | `string` | Principal's identifier (e.g., `"user@acme.com"`, `"marketdata.example.com"`) | -| `scopes` | `repeated string` | Scopes granted — convenience mirror of the token's `scope` claim | +| `scopes` | `repeated string` | Mirror of the terminal token's effective scopes | | `expires_at` | `Timestamp` | When this delegation expires. Exchange MUST reject expired tokens | | `max_spend_cents` | `int64` (optional) | Maximum spend in currency minor units (e.g. cents for USD) | -| `token` | `bytes` | Token bytes — a JWT (base64url JWS) by default | +| `token` | `bytes` | The complete chain as one compact JWS, at most 32768 bytes (a wire rule, `bytes.max_len`) | | `token_format` | `string` | Token format: `"jwt"` (the sole/default format) | -| `revocation_uri` | `string` (optional) | URI for real-time revocation checking (high-value transactions) | +| `revocation_uri` | `string` (optional) | Informational. Revocation lookup is not part of this profile; a chain is invalidated early by withdrawing the configured key (see [Verification](#verification)) | | `max_accesses` | `int32` (optional) | Maximum number of accesses allowed under this delegation. Exchange tracks count and denies with `DENIAL_REASON_QUOTA_EXCEEDED` when count >= limit. | | `quota_period` | `Duration` (optional) | How often access/spend counters reset. E.g., `"2592000s"` (30 days) for monthly subscriptions — proto-JSON encodes `Duration` as seconds. When absent, quota is lifetime (bounded only by `expires_at`). | -| `issuer` | `string` (optional) | Token issuer — OIDC issuer URL (for JWT signature validation via OIDC→JWKS) | +| `issuer` | `string` (optional) | Mirror of the root token's `iss`, the configured trust anchor's identity. Informational: no key is ever looked up from it | | `ext` | `Struct` | Implementer-specific extensions | | `ext_critical` | `repeated string` | Critical extension keys (COSE crit pattern) | -The `scopes` field is a convenience mirror — the Exchange MAY use it for fast catalog filtering before performing full token verification. The token's claims are authoritative. +Every field except `token` and `token_format` is a mirror of the signed chain (see [Mirror fields](#mirror-fields)). The Exchange MAY use `scopes` for fast catalog filtering before it verifies the token; the token's claims are authoritative. + +### Chain encoding and limits + +`Delegation.token` is **one** compact JWS: three non-empty segments of unpadded base64url separated by dots, with no other character anywhere, whitespace included. The header segment decodes to one JSON object and the payload segment to exactly one JSON object with nothing after it. The chain is nested, not listed: + +- Every child token carries its **complete parent**, as the parent's compact serialization, in the string claim `fora_parent`. The parent's signature is therefore inside the child's signed payload, so a child cannot be re-attached to a different parent. +- The **root token has no `fora_parent`** claim: the innermost token of the chain is the root, and it is verified with a configured key, never with a key it carries. +- A chain holds **at most 4 tokens**: the root and up to three children. The verifier never decodes a fifth token. +- The complete `Delegation.token` is **at most 32768 bytes**. The limit is a wire rule, `bytes.max_len` on the field, so an oversized token is refused as an invalid request before any JWT is parsed, whether or not the bytes are a valid JWT. +- Before each nested decode the verifier checks the **remaining depth** (fewer than 4 tokens read so far) and that the embedded parent string is **not longer than 32768 bytes**; only then does it parse the parent. The verifier never fetches an ancestor from anywhere: every token it needs is inside the one it was given. + +### Signing algorithm and protected header + +Every token in the chain is signed with an **Ed25519** key, and the verifier accepts exactly two names for it in the protected header: + +| Header | Required? | Allowed values | +|---|---|---| +| `alg` | **Yes** | `EdDSA` ([RFC 8037](https://www.rfc-editor.org/rfc/rfc8037)) or `Ed25519` ([RFC 9864](https://www.rfc-editor.org/rfc/rfc9864)). In both cases the signing key MUST be an Ed25519 OKP key; any other algorithm or curve is rejected. A chain may mix the two names. | +| `typ` | **Yes** | The exact string `fora-delegation+jwt`, on every token. A token with another `typ`, or none, is rejected. | +| `jwk` | **Yes** on every child; ignored on the root | The signing key as a public JWK: `{"kty":"OKP","crv":"Ed25519","x":""}`. Its RFC 7638 thumbprint MUST equal the parent's `cnf.jkt`. A `jwk` on the root is not read; the root is verified only with the configured key. | +| `kid` | No | Informational; the verifier does not use it to choose a key. | +| `crit` | MUST NOT be present | The profile defines no extension header parameters, so a token that marks any as critical is rejected. | +| `b64` | MUST NOT be present | Unencoded payloads ([RFC 7797](https://www.rfc-editor.org/rfc/rfc7797)) are not part of the profile. A token that carries `b64` is malformed, with or without `crit`. | + +The verifier checks the signature over the **original signed bytes** (the base64url header and payload exactly as received). It never re-serializes or rewrites a header, so a token stays valid or invalid regardless of the library that produced it. ### Delegation-Claims Profile -The `token`'s claims are the **authoritative** source of the grant (the plaintext `Delegation` fields are only the convenience mirror). FORA defines a small **registered claim vocabulary** — the same registered-tokens + optional-fields + vendor-namespace pattern used elsewhere in the protocol: +The token's claims are the **authoritative** source of the grant; the plaintext `Delegation` fields are only a mirror (see [Mirror fields](#mirror-fields)). -| Claim | JWT claim | Required? | Meaning | -|---|---|---|---| -| holder | `cnf` (with `jkt`) | **Yes** | Binds the grant to a key by RFC 7638 thumbprint — verified per [Verification](#verification) (the agent's key the request proves possession of MUST hash to `cnf.jkt`). Without it the token is bearer-usable. | -| issuer | `iss` | Yes | Who granted it (resource owner / principal). | -| `scope` | `scope` | No | Granted scopes (space-delimited; segment-wise matching — see [Scope Matching](#scope-matching)). | -| `exp` | `exp` | No (recommended) | Expiry. | -| `max_spend_cents` | `fora_max_spend_cents` | No | Spend cap (currency minor units). | -| `max_accesses` | `fora_max_accesses` | No | Access-count cap. | -| `quota_period` | `fora_quota_period` | No | Counter reset period. | +| Claim | Required? | Type and rule | +|---|---|---| +| `iss` | **Yes** | String. On the root: the identity configured with the trusted key, matched exactly (see [Issuers and trust](#issuers-and-trust)). On a child: the RFC 7638 thumbprint of the child's own signing key, which is also the parent's `cnf.jkt`. | +| `aud` | **Yes** | A string or a non-empty array of strings. Each value names a recipient by the proto's Request recipient rule (see [Audience](#audience)). A child's audiences MUST be a subset of its parent's. | +| `cnf` with `jkt` | **Yes** | Object with the string member `jkt`: the base64url RFC 7638 SHA-256 thumbprint (43 characters, no padding) of the Ed25519 public JWK the grant is bound to. | +| `exp` | **Yes** | A finite JSON number: NumericDate seconds since the epoch ([RFC 7519](https://www.rfc-editor.org/rfc/rfc7519)). A child's `exp` MUST NOT be later than its parent's. | +| `nbf` | No | A finite JSON number, NumericDate. Honored on every token that carries it. | +| `jti` | Recommended | A non-empty string, unique per issuer. Present so a future revocation check can name the token; the verifier checks only its shape. | +| `iat`, `sub` | No | Accepted and not interpreted. | +| `scope` | No | Space-separated scopes. A missing or empty `scope` grants **no** scoped access; the token is still valid for holder binding alone. A child's scopes MUST be covered by its parent's (see [Scope Matching](#scope-matching)): `publisher:news:read` under `publisher:*:read` is valid narrowing, `publisher:news:*` under `publisher:*:read` widens and is rejected. | +| `fora_parent` | **Yes** on every child; MUST be absent on the root | The complete parent token as a compact JWS string. | +| `fora_max_spend_cents` | No | Integer, currency minor units. Mirrors `Delegation.max_spend_cents`. | +| `fora_max_accesses` | No | Integer. Mirrors `Delegation.max_accesses`. | +| `fora_quota_period` | No | Duration string in seconds, the proto-JSON form (`"2592000s"`). Mirrors `Delegation.quota_period`. | +| `fora_advisory` | No | A JSON object of informational members the verifier does not interpret (see [Advisory information](#advisory-information)). | Rules: -- **Everything except the holder binding (`cnf`) is optional.** An issuer that only wants scope + expiry sets just those; absent caps mean "no cap from the delegation" (the Exchange's own limits still apply). -- **Vendors MAY add namespaced claims** (`vendor:claim`). -- **Constraints fail closed.** A claim a verifier cannot evaluate is **binding by default** — it MUST reject rather than ignore (a dropped constraint would fail *open*, granting more than intended). This mirrors [`Restriction.advisory`](/protocol/licensing-terms/): the issuer may mark a custom claim *advisory* to downgrade it to ignorable. (The issuer lists the mandatory-to-understand claims; an unrecognized non-advisory claim is rejected.) -- **`fora_`-prefixed claim names are reserved** for this registry. Vendors MUST use their own `vendor:` namespace and MUST NOT define `fora_*` names, so registered claims can never be shadowed. -- **Claim values keep their home encoding.** RFC-registered claims use the encoding their RFC defines — `exp` is a NumericDate (seconds since epoch, RFC 7519). `fora_*` claims mirror a `Delegation` field and use that field's proto-JSON encoding — for example, `fora_quota_period` is a Duration string in seconds (`"2592000s"`), the same form the [`quota_period` field](/reference/proto-fora/#delegation) carries on the wire. +- **Any other top-level claim is rejected.** A claim this table does not list is a mandatory restriction the verifier cannot evaluate, and a dropped constraint would fail *open*. Vendors put their metadata inside `fora_advisory`; they MUST NOT define top-level `fora_*` or other claims. +- **Claim values keep their home encoding.** RFC-registered claims use the encoding their RFC defines; `fora_*` claims that mirror a `Delegation` field use that field's proto-JSON encoding, so `fora_quota_period` is the same `"2592000s"` string the [`quota_period` field](/reference/proto-fora/#delegation) carries on the wire. +- **A wrong type is a rejection.** A non-numeric `exp`, an empty `jti`, a `cnf` without `jkt` or an `aud` that is not a string or array of strings makes the token invalid; the verifier never coerces a value. + +### Time and clock leeway + +Times are NumericDate seconds, as JSON numbers between **0** and **253402300799** (`9999-12-31T23:59:59Z`, the last second a protocol `Timestamp` carries); a value outside that range is a bad claim, so the effective expiry the verifier returns always fits a `Timestamp`. The verifier evaluates every token in the chain against one clock reading `now` with a fixed leeway of **60 seconds**: + +- A token is **expired** when `now >= exp + 60`. +- A token is **not yet valid** when it carries `nbf` and `now < nbf - 60`. +- Leeway is for clock skew only. The **effective expiry** the verifier returns is the smallest `exp` in the chain, without leeway, and an Exchange never issues a gated offer that expires later than it. + +### Issuers and trust + +Trust is configured, never discovered. The verifier is given a list of trusted root keys, each with the issuer identity it belongs to; in an Exchange that list comes from the subscription record that owns the gated term. The verifier: + +- verifies the root token with one of those keys and requires the root's `iss` to equal that key's configured identity, as an exact string. The identity is normally the issuer's domain (`marketdata.example.com`), but it is an opaque label: it need not equal the publisher segment of a subscription scope, because a subscription provider may be a different party from the publisher; +- requires every child's `iss` to equal the RFC 7638 thumbprint of the key that signed it, which is the parent's `cnf.jkt`. This makes the intermediate issuer checkable with no extra configuration: a child whose signature and `jwk` are valid but whose `iss` names something else is rejected; +- never fetches a key through OIDC discovery, a JWKS endpoint, a `jku`/`x5u` header or any token-supplied URL, and never trusts a `jwk` on the root. A key that is not configured verifies nothing. + +Removing a key from the configured list withdraws trust from every token it signed; a token that was valid yesterday is refused at the next request. + +### Audience -> **Token format.** The delegation token is a **holder-bound JWT** — `token_format: "jwt"` is the sole format conformant implementations support. Multi-hop attenuation is expressed as a chain of `cnf`-linked child JWTs (each signed by the key its parent named, each narrowing scope), and the holder binding always applies (the key the request proves possession of — the request-signing key on a direct call, the `AgentAcceptance` key on a purchase a Broker relayed — must match the holder the token is sealed to). +Each `aud` value is a recipient identity by the proto's **Request recipient** rule, the same rule the `exchange` field of a request follows: + +1. Validate the value **as sent**: a bare host with an optional port `1-65535`, ASCII only, no scheme, path, query, userinfo or trailing dot. +2. Only then lowercase it, and treat an absent port and `:443` as the same port. +3. Compare it **exactly** with the verifier's own identity. A subdomain is a different party. + +Several audiences are listed explicitly; there is no wildcard audience. A child MAY drop audiences its parent listed and MUST NOT add one. A chain whose terminal token does not name the verifier is rejected. + +### Mirror fields + +The plaintext `Delegation` fields repeat values from the signed chain so an Exchange can filter cheaply before it verifies. They establish nothing: a verifier reads the chain, and a mirror that disagrees with it cannot grant a scope, extend validity or raise a cap. + +| Field | Mirrors | +|---|---| +| `issuer`, `principal_domain` | The **root** token's `iss` — the trust anchor's identity, never an intermediate or terminal issuer. | +| `principal_id` | Informational identifier of the principal; not read by the verifier. | +| `scopes` | The terminal token's effective scopes. | +| `expires_at` | The chain's earliest `exp`. | +| `max_spend_cents`, `max_accesses`, `quota_period` | The terminal token's effective caps. | + +### Inherited restrictions + +Every restriction travels down the chain. The verifier checks every ancestor, not only the terminal token: + +- **Scopes narrow only.** The effective scopes are the terminal token's `scope`, and each token's scopes are covered by its parent's. +- **Expiry narrows only.** A child's `exp` is no later than its parent's, and every token is checked against the clock. The effective expiry is the earliest `exp` in the chain. +- **Omitting a restriction does not remove it.** A child without `fora_max_spend_cents` is still bound by its parent's cap. A child that states a cap MUST NOT state one higher than its parent's. +- **Caps are shared, not multiplied.** A parent's spend or access limit applies to all descendants together; issuing three children does not create three allowances. The effective cap is the smallest in the chain. +- **Unsupported mandatory restrictions reject.** A verifier that cannot enforce a cap the chain carries, or meets a claim it does not know, rejects the delegation rather than ignoring the restriction. The Go SDK verifier of this release meters nothing, so it refuses any chain that carries `fora_max_spend_cents`, `fora_max_accesses` or `fora_quota_period`; an Exchange that enforces caps verifies them in its own step. + +### Advisory information + +Optional vendor metadata is carried **inside** the signed `fora_advisory` object, and only there. The verifier does not interpret its members, so an unknown name there is never a rejection. Two limits keep it from weakening the chain: + +- A `fora_advisory` member that uses the name of a profile claim (`scope`, `exp`, `aud`, `fora_max_spend_cents` and the rest) is rejected: an authorization claim cannot be downgraded to advisory information. +- `fora_advisory` cannot loosen an ancestor. A child's advisory object says nothing about its parent's mandatory claims, which are checked as written. ### Delegation chain example -MarketData (resource owner) issues an authority JWT to the Acme principal, bound to Acme's key: +MarketData (the trusted issuer) issues a root token to the Acme principal, bound to Acme's key. The verifier's configured entry for this issuer is the identity `marketdata.example.com` with MarketData's Ed25519 public key. ```json -// authority JWT — signed by marketdata.example.com (the owner key) -// header -{ "alg": "EdDSA", "typ": "JWT" } +// root token — signed by marketdata.example.com with the configured key +// protected header +{ "alg": "Ed25519", "typ": "fora-delegation+jwt" } // claims { "iss": "marketdata.example.com", + "aud": ["exchange.example.com"], + "jti": "md-2026-000481", "scope": "quote:* earnings:*", - "max_spend_cents": 50000, "exp": 1781787345, "cnf": { "jkt": "" } } ``` -Acme delegates to a research agent that only needs earnings data, narrowing scope and shortening the TTL — no call back to MarketData: +Acme delegates to a research agent that only needs earnings data, narrowing scope and shortening the TTL — no call back to MarketData. The child names its own signing key as `iss`, carries that key in `jwk`, and carries the complete root token in `fora_parent`: ```json -// delegation JWT — signed by Acme's principal key -// header carries the principal pubkey so the verifier links it to authority.cnf -{ "alg": "EdDSA", "typ": "JWT", +// child token — signed by Acme's principal key, the key root.cnf.jkt names +// protected header +{ "alg": "EdDSA", "typ": "fora-delegation+jwt", "jwk": { "kty": "OKP", "crv": "Ed25519", "x": "" } } // claims { - "iss": "acme", + "iss": "", + "aud": ["exchange.example.com"], + "jti": "acme-research-0007", "scope": "earnings:*", "exp": 1781701545, - "cnf": { "jkt": "" } + "cnf": { "jkt": "" }, + "fora_parent": "" } ``` -The agent then signs its request (RFC 9421) with its own key. **Zero round trips to MarketData** — each delegation is a locally-issued child JWT. +`Delegation.token` carries the child token only. The agent then signs its request (RFC 9421) with its own key. **Zero round trips to MarketData** — each delegation is a locally-issued child JWT. The chain's effective expiry is `1781701545`, the child's `exp`. The `fora_max_spend_cents` claim on the root is a cap this release's Go verifier refuses, as [Inherited restrictions](#inherited-restrictions) explains; an Exchange that meters spend verifies it in its own step. + +A chain of exactly this shape, signed with the fixed test keys of the shared chain cases, is the case `two_tokens`: the root (`marketdata.example`, `quote:* earnings:*`, `alg: EdDSA`) inside a child (`earnings:*`, `alg: Ed25519`), addressed to `exchange.example`. The Go SDK re-verifies it on every test run: + +{/* delegation-chain: two_tokens */} +```text +eyJhbGciOiJFZDI1NTE5IiwiandrIjp7ImNydiI6IkVkMjU1MTkiLCJrdHkiOiJPS1AiLCJ4IjoieUYwU2RxTHZHZnlIVjRMV2hMTS1nTXg3clBnX0xNeFpLcHBKZFk3blJ2WSJ9LCJ0eXAiOiJmb3JhLWRlbGVnYXRpb24rand0In0.eyJhdWQiOlsiZXhjaGFuZ2UuZXhhbXBsZSJdLCJjbmYiOnsiamt0IjoiV2ktdk5Udk84NHBvZEpwSXZYS1VsR05EVXVoS0pLUzR6VnpoblJ2VDNZNCJ9LCJleHAiOjE3ODAzMTg4MDAsImZvcmFfcGFyZW50IjoiZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNkltWnZjbUV0WkdWc1pXZGhkR2x2Yml0cWQzUWlmUS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVaWGhoYlhCc1pTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaVdHUjNZWEpFTkV3d01WQlpObEZhTUhSaE16QnBia0pNUjNaVU5UZHBkRTFVV1Vsa01raDZRM0ZTUlNKOUxDSmxlSEFpT2pFM09EQXpNVGc0TURBc0ltbHpjeUk2SW0xaGNtdGxkR1JoZEdFdVpYaGhiWEJzWlNJc0luTmpiM0JsSWpvaWNYVnZkR1U2S2lCbFlYSnVhVzVuY3pvcUluMC5pa0tTX2tzVkZTSzk2eE9ja2ZjMW8zc2prd1VpUUhMSUFpNFpCM3B1eHdmMWJQOTgzcGl6RkhLNy16Q2pid2VLVmNWaFczeTlkOEEyTTJnVGxtTk5BUSIsImlzcyI6Ilhkd2FyRDRMMDFQWTZRWjB0YTMwaW5CTEd2VDU3aXRNVFlJZDJIekNxUkUiLCJzY29wZSI6ImVhcm5pbmdzOioifQ.BYPx8_MNws93olALVra3qZDnx-Xw9wIhtansjrBcAJzEid1psR764LvcyE0pdJ1GhXfuMd9SYWRHQHRy6fOoCA +``` ### Verification -The Exchange/edge verifies a delegation by (offline, using only the owner's public key): +The Exchange or edge verifies a delegation offline, with the configured issuer keys as its only input besides the request. Every step fails closed. What a failure produces depends on where it happens: + +- A `token` over 32768 bytes fails **wire validation**: the whole request is refused as invalid, before any item is read, like any other field that breaks a wire rule. +- At **discovery**, a chain that fails any later step is treated as if no delegation were present: the terms gated behind it are hidden (see [Existence hiding](#existence-hiding)), and no denial reason reports why. The Exchange MAY journal the failure. +- At **purchase**, a chain that fails any later step denies the item with `DENIAL_REASON_DELEGATION_INVALID`. -1. **Verify the authority JWT** with the resource owner's published key (the trust anchor). Read its `scope`, `exp`, and `cnf.jkt`. -2. **Verify each delegation JWT** with the key in its own JOSE header `jwk` — but only after checking `thumbprint(header key) == parent cnf.jkt` (the **chain-linkage invariant**). Reject a JWT signed by a key the parent did not name. Check each child's `scope` is covered by its parent's. -3. **Verify holder binding** — the agent's key that the request proves possession of MUST hash to the final delegation's `cnf.jkt`: on a direct call the key that signed the HTTP request ([RFC 9421](#request-signatures-rfc-9421)), on a purchase a Broker relayed the key that signed the agent's `AgentAcceptance` ([Which signature binds in a brokered request](#verification)). If not, **reject** (`DENIAL_REASON_DELEGATION_INVALID`). On a query a Broker originated, which proves possession of no agent key, the delegation is not verified at all: it is not honoured, and the query is answered as if none were present. This is what makes a leaked token useless to a thief: possessing the bytes is not enough — you must control the bound private key. It is **not optional** and **not skippable**; an Exchange that omits it reduces the token to a bearer credential and breaks the anti-theft guarantee. -4. Check `exp` on every JWT in the chain against current time. -5. Optionally check `revocation_uri` for high-value transactions. +The Go SDK implements exactly this order in `VerifyDelegation`, and the shared chain cases in `sdk/go/helpers/testdata/delegation-chains.json` pin which check refuses which input: -This is entirely offline — no callback to the issuer. The verifier needs only the owner's published public key (cacheable); the principal and agent keys arrive inside the chain (header `jwk` + `cnf.jkt`). +1. **Size and shape.** The `Delegation.token` is at most 32768 bytes (checked at wire validation and again here, before parsing) and is one compact JWS with the segment rules above. `token_format` is `"jwt"` or empty. +2. **Walk the chain inward.** Decode the outer token's protected header and claims and check them against the profile: `alg` is `EdDSA` or `Ed25519`, `typ` is `fora-delegation+jwt`, no `crit`, the required claims are present with the right types, no unknown top-level claim, no cap claim the verifier cannot enforce, no profile name inside `fora_advisory`. If the token carries `fora_parent`, check the remaining depth and the parent's size, then decode the parent the same way, and repeat until a token without `fora_parent` is reached: that token is the root. A fifth token or an oversized parent is a rejection. +3. **Verify the root** with the configured key whose identity equals the root's `iss`. No configured key for that identity, or a signature that does not verify, is a rejection. The root's `jwk`, if any, is ignored. +4. **Verify each child** outward from the root: its protected `jwk` is an Ed25519 OKP public key, `thumbprint(jwk) == parent.cnf.jkt` (the **chain-linkage invariant**), its `iss` equals that thumbprint, and its signature verifies with that key over the original signed bytes. +5. **Check time on every token** with the 60-second leeway: not expired, not before `nbf`, and each child's `exp` no later than its parent's. +6. **Check attenuation on every child:** audiences a subset of the parent's, scopes covered by the parent's. +7. **Check the audience on every token:** `aud` names the verifier's own identity by the Request recipient rule. +8. **Verify holder binding** — the agent's key that the request proves possession of MUST hash to the terminal token's `cnf.jkt`: on a direct call the key that signed the HTTP request ([RFC 9421](#request-signatures-rfc-9421)), on a purchase a Broker relayed the key that signed the agent's `AgentAcceptance` ([Which signature binds in a brokered request](#verification)). On a query a Broker originated, which proves possession of no agent key, the delegation is not verified at all: it is not honored, and the query is answered as if none were present. This is what makes a leaked token useless to a thief: possessing the bytes is not enough — you must control the bound private key. It is **not optional** and **not skippable**; an Exchange that omits it reduces the token to a bearer credential and breaks the anti-theft guarantee. +9. **Return the verified result:** the root issuer identity, the terminal holder thumbprint, the effective scopes and the effective expiry. A gated offer the Exchange then issues MUST NOT expire later than that effective expiry. -**Binding across hops.** Each delegation re-binds to the next holder's key (`cnf.jkt` of the child = the next agent), so at every hop the bound key is whoever now signs the request. The chain of JWT signatures carries the binding forward — there is no point at which the token reverts to bearer form. +Revocation lookup through `revocation_uri` is not part of this profile; withdrawing a configured key is the way to invalidate a chain early. -**Which signature binds in a brokered request.** The holder-binding check always matches the **terminal holder** — the key named in the *final* delegation's `cnf.jkt` — and that key is always a key the request proves possession of. On a direct call the proof is the agent's RFC 9421 request signature, so `cnf.jkt` is checked against the key that signed the request. On a purchase relayed by a Broker (`BrokerService.ExecuteTransaction`), the Broker signs the transport request alone, so `cnf.jkt` is checked against the key that signed the agent's `AgentAcceptance` in the body, and an item without an `AgentAcceptance` fails the binding. The delegation therefore stays bound to the agent's private key through the Broker, exactly as on a direct call. A Broker is never delegated to and never becomes the holder. On a query a Broker originated (Broker-led discovery), the Broker signs alone, so a delegation on it cannot pass holder binding: the Exchange does not honour it 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. An Exchange that requires a verified delegation before it presents a scoped term ([Scope-Based Access](#scope-based-access)) therefore presents no such term on that query; the agent can query that Exchange directly, where its own request signature passes the holder binding. +**Binding across hops.** Each child re-binds to the next holder's key (`cnf.jkt` of the child = the next agent), so at every hop the bound key is whoever now signs the request. The chain of JWT signatures carries the binding forward — there is no point at which the token reverts to bearer form. + +**Which signature binds in a brokered request.** The holder-binding check always matches the **terminal holder** — the key named in the *last* child's `cnf.jkt` — and that key is always a key the request proves possession of. On a direct call the proof is the agent's RFC 9421 request signature, so `cnf.jkt` is checked against the key that signed the request. On a purchase relayed by a Broker (`BrokerService.ExecuteTransaction`), the Broker signs the transport request alone, so `cnf.jkt` is checked against the key that signed the agent's `AgentAcceptance` in the body, and an item without an `AgentAcceptance` fails the binding. The delegation therefore stays bound to the agent's private key through the Broker, exactly as on a direct call. A Broker is never delegated to and never becomes the holder. On a query a Broker originated (Broker-led discovery), the Broker signs alone, so a delegation on it cannot pass holder binding: the Exchange does not honor it 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. An Exchange that requires a verified delegation before it presents a scoped term ([Scope-Based Access](#scope-based-access)) therefore presents no such term on that query; the agent can query that Exchange directly, where its own request signature passes the holder binding. ### Delegation Denial Reasons @@ -254,17 +361,17 @@ When delegation verification fails, the Exchange returns a specific denial reaso | Denial Reason | Meaning | |---|---| | `DENIAL_REASON_QUOTA_EXCEEDED` | Subscription access count exhausted for this period | -| `DENIAL_REASON_DELEGATION_INVALID` | Delegation token missing, unverifiable (broken chain linkage or holder-binding mismatch), expired, or it widens scope beyond its parent | +| `DENIAL_REASON_DELEGATION_INVALID` | On a purchase item: delegation token missing, malformed, unverifiable (broken chain linkage, wrong issuer or holder-binding mismatch), expired, not valid yet, addressed to another recipient, or it widens scope, audience or a cap beyond its parent. A token over 32768 bytes never reaches an item: wire validation refuses the request. At discovery none of these is reported; the gated terms are hidden instead | ### Libraries | Language | Library | Notes | |---|---|---| -| Go | `golang-jwt/jwt` | EdDSA (Ed25519) signing/verification, JOSE headers | -| TypeScript | `jose` | EdDSA, runs in Node and the browser | -| Python | `PyJWT` (+ `cryptography`) | EdDSA | +| Go | `sdk/go/helpers` (`VerifyDelegation`) | The reference verifier of this profile. It checks the Ed25519 signature with the standard library under either `alg` name and uses go-jose for the `jwk` header and the RFC 7638 thumbprint, because go-jose alone does not accept `alg: Ed25519`. Issuers may sign with go-jose (`EdDSA`) or any JOSE library. | +| TypeScript | `jose` (5.10 or later) | Signs and verifies under `Ed25519` and `EdDSA`; runs in Node and the browser. A TypeScript chain verifier is deferred (see the parity matrix). | +| Python | `joserfc` or `PyJWT` (+ `cryptography`) | Signs under `Ed25519` or `EdDSA`. A Python chain verifier is deferred (see the parity matrix). | -All use Ed25519 natively — the same key type as FORA identity signatures — so the delegation token and the RFC 9421 request signature share one key type and one library surface. +All use Ed25519 natively — the same key type as FORA identity signatures — so the delegation token and the RFC 9421 request signature share one key type. ## Scope-Based Access @@ -292,16 +399,20 @@ Scopes follow the pattern `{domain}:{permission}` or `{profile}:{permission}`: ### Scope Matching -Matching is **segment-wise** (`:`-separated). A granted scope **G** covers a required scope **R** iff, comparing segment by segment, each G segment equals the corresponding R segment **or** is `*`; a **terminal `*` matches all remaining segments**. There is **no implicit prefix match**. A grant that is **more specific than what is required does not cover it** — coverage requires G to be equal-to-or-broader-than R, never narrower (e.g. granted `dist:US:CA` does not cover required `dist:US`). +Matching is **segment-wise** (`:`-separated). A granted scope **G** covers a required scope **R** iff, comparing segment by segment, each G segment equals the corresponding R segment **or** is `*`; a **terminal `*` matches all remaining segments**, of which there must be at least one. There is **no implicit prefix match**. A grant that is **more specific than what is required does not cover it** — coverage requires G to be equal-to-or-broader-than R, never narrower (e.g. granted `dist:US:CA` does not cover required `dist:US`, and granted `dist:US:*` does not cover required `dist:US`). A `*` in the required scope is a literal segment, so a narrow grant never covers a wildcard requirement; this is what stops a delegation child from widening its parent's grant. Matching is case-sensitive and never trims. One algorithm applies protocol-wide: to `Requester.scopes` against `LicenseTerm.scopes`, and to a child delegation's scopes against its parent's. | Granted | Covers | Does **not** cover | |---|---|---| -| `dist:*` | `dist:US`, `dist:US:CA` | — | -| `dist:US:*` | `dist:US:CA` | `dist:EU` | +| `dist:*` | `dist:US`, `dist:US:CA`, `dist:US:*` | `dist` (a terminal `*` needs a segment to match) | +| `dist:US:*` | `dist:US:CA` | `dist:EU`, `dist:US` | +| `publisher:*:read` | `publisher:news:read`, `publisher:*:read` | `publisher:news:write`, `publisher:news:*` (the required `*` is literal) | | `dist` | `dist` | `dist:US` | | `dist:US:CA` | `dist:US:CA` | `dist:US` (grant narrower than required) | +| `credit:read` | `credit:read` | `*`, `Credit:read` | | `*` | everything | — | +A set of granted scopes covers a set of required scopes when every required scope is covered by at least one granted scope. An empty required set is always covered; an empty granted set covers nothing else. The SDKs expose this as `ScopeCovers` / `ScopesCover` (`scope_covers` / `scopes_cover`, `scopeCovers` / `scopesCover`), and `ScopesSubset` is the same check read from the child's side. + ### Access Tiers **No scopes (empty):** Exchange applies its default access policy — typically all publicly available resources. This is the common case for public agents. @@ -606,8 +717,9 @@ The Exchange verifies the signature by fetching the key directory at `https://re ### Subscription Agent (MarketData JWT) -A financial analysis agent carries a MarketData-issued delegation JWT that grants access to subscription-tier data. `delegation.token` is a `bytes` field, so proto-JSON carries the compact JWS base64-encoded (decoded here for illustration: `eyJhbGciOiJFZERTQSJ9..`). +A financial analysis agent carries a MarketData-issued root token that grants access to subscription-tier data. `delegation.token` is a `bytes` field, so proto-JSON carries the compact JWS base64-encoded. The token below is a real one from the shared chain cases (`docs_subscription_agent`): it verifies with MarketData's configured key, is addressed to `exchange.example`, and is bound to FinBot's request key. +{/* delegation-chain: docs_subscription_agent */} {/* fora-validate: ResourceQuery */} ```json { @@ -627,9 +739,8 @@ A financial analysis agent carries a MarketData-issued delegation JWT that grant "principal_domain": "marketdata.example.com", "principal_id": "marketdata.example.com", "scopes": ["subscription:marketdata-2026", "quote:*", "earnings:*"], - "expires_at": "2026-03-20T18:00:00Z", - "max_spend_cents": 50000, - "token": "ZXlKaGJHY2lPaUpGWkVSVFFTSjkuPGNsYWltczogaXNzLCBzY29wZSwgZXhwLCBjbmYuamt0Pi48c2lnPg", + "expires_at": "2026-06-01T13:00:00Z", + "token": "ZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNkltWnZjbUV0WkdWc1pXZGhkR2x2Yml0cWQzUWlmUS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVaWGhoYlhCc1pTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaWJUQk1PRGh0Y1V4M01uWnpSbnBNWTBGSFZHbE5kV2xPYWpZM1JqRjBRWGxCYjJSb09XMTNTM3A0T0NKOUxDSmxlSEFpT2pFM09EQXpNVGc0TURBc0ltbHpjeUk2SW0xaGNtdGxkR1JoZEdFdVpYaGhiWEJzWlM1amIyMGlMQ0pxZEdraU9pSnRaQzF6ZFdJdE1qQXlOaTB3TkRneElpd2ljMk52Y0dVaU9pSnpkV0p6WTNKcGNIUnBiMjQ2YldGeWEyVjBaR0YwWVMweU1ESTJJSEYxYjNSbE9pb2daV0Z5Ym1sdVozTTZLaUo5LmQ3UjRnYVhjQ2d5OW5SMzA0bF9LeE85c1FFV3BLRGs4dVY0UEVuSXN6ZS1RbnJfSjBfOV9fbTNnVzhiR0ZlaWJXZXp1VEI4akYyRzdKbGVsaW1vaEFn", "token_format": "jwt" } }, @@ -645,14 +756,15 @@ A financial analysis agent carries a MarketData-issued delegation JWT that grant The Exchange: 1. Verifies the request's RFC 9421 HTTP Message Signature against `fintech.example.com`'s published key -2. Verifies the delegation JWT — signed by `marketdata.example.com`'s key — and checks its `cnf.jkt` equals the thumbprint of the request signer (holder binding) -3. Reads the scopes the token grants from its `scope` claim (`subscription:marketdata-2026`, `quote:*`, `earnings:*`) -4. Presents the subscription-tier terms (zero marginal cost) whose scopes, such as `subscription:marketdata-2026`, the request states. This Exchange also requires a verified delegation that grants a subscription term's scopes before it presents the term, which the token in step 3 does ([Scope-Based Access](#scope-based-access)) +2. Verifies the delegation chain as [Verification](#verification) describes: the root verifies with the key configured for the issuer `marketdata.example.com` on the subscription record, `aud` names this Exchange, and `cnf.jkt` equals the thumbprint of the request signer (holder binding) +3. Reads the effective scopes the verifier returns (`subscription:marketdata-2026`, `quote:*`, `earnings:*`) and the effective expiry (`2026-06-01T13:00:00Z`) +4. Presents the subscription-tier terms (zero marginal cost) whose scopes, such as `subscription:marketdata-2026`, the verified delegation covers. For such a gated term the Exchange reads scopes from the verified delegation only and ignores `requester.scopes`; the non-gated terms are still filtered by the stated scopes ([Scope-Based Access](#scope-based-access)). A gated offer expires no later than `2026-06-01T13:00:00Z` ### Enterprise Agent (IAM-Issued JWT) -An internal agent at a law firm carries a delegation JWT issued by the firm's IAM system, granting access to legal and credit resources. +An internal agent at a law firm carries a root token issued by the firm's IAM system, granting access to legal and credit resources. The token is the shared chain case `docs_enterprise_agent`. +{/* delegation-chain: docs_enterprise_agent */} {/* fora-validate: ResourceQuery */} ```json { @@ -671,9 +783,8 @@ An internal agent at a law firm carries a delegation JWT issued by the firm's IA "principal_domain": "iam.kirkland.com", "principal_id": "user@kirkland.com", "scopes": ["legal:case-law", "legal:statutes", "credit:read"], - "expires_at": "2026-03-20T20:00:00Z", - "max_spend_cents": 100000, - "token": "ZXlKaGJHY2lPaUpGWkVSVFFTSjkuPGNsYWltczogaXNzLCBzY29wZSwgZXhwLCBjbmYuamt0Pi48c2lnPg", + "expires_at": "2026-06-01T13:00:00Z", + "token": "ZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNkltWnZjbUV0WkdWc1pXZGhkR2x2Yml0cWQzUWlmUS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVaWGhoYlhCc1pTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaVJtVm1Ta3N5VDB0YVNtSkNVbGxqZW1kTU9IbFphelJyVXpWVVNGOVVVbW94ZDNJM1IxSnlhVE15TkNKOUxDSmxlSEFpT2pFM09EQXpNVGc0TURBc0ltbHpjeUk2SW1saGJTNXJhWEpyYkdGdVpDNWpiMjBpTENKcWRHa2lPaUpwWVcwdE4yTXhaU0lzSW5OamIzQmxJam9pYkdWbllXdzZZMkZ6WlMxc1lYY2diR1ZuWVd3NmMzUmhkSFYwWlhNZ1kzSmxaR2wwT25KbFlXUWlMQ0p6ZFdJaU9pSjFjMlZ5UUd0cGNtdHNZVzVrTG1OdmJTSjkucjZuYXhKa08tT2FDU0czbENnYXNGRXVFUDktbHg3UFNDVU1ydG1pa1A5YS1rbkVndTlyR1FwYk9kVTdPbWlpc2Jkb1RYT1g3SEh4dmxsN1pzdFRNQ1E=", "token_format": "jwt" } }, @@ -687,12 +798,13 @@ An internal agent at a law firm carries a delegation JWT issued by the firm's IA } ``` -The internal Exchange verifies the delegation JWT against `iam.kirkland.com`'s published key. If the agent requests a resource outside its scopes, that URI's group comes back with no offers and no absence reason, the same answer as for a resource the Exchange has nothing for (see [Existence hiding](#existence-hiding)). +The internal Exchange verifies the chain with the key it has configured for the issuer `iam.kirkland.com`; nothing is fetched from IAM. If the agent requests a resource outside its scopes, that URI's group comes back with no offers and no absence reason, the same answer as for a resource the Exchange has nothing for (see [Existence hiding](#existence-hiding)). ### Delegated Sub-Agent (cnf-chained delegation) -The Kirkland legal agent from the previous example delegates to a sub-agent that only needs credit data. It issues a child delegation JWT (bound to the sub-agent's key) that removes legal access and lowers the spend cap. +The Kirkland legal agent from the previous example delegates to a sub-agent that only needs credit data. It issues a child token, bound to the sub-agent's key, that removes legal access and expires 30 minutes earlier. The token is the shared chain case `docs_delegated_sub_agent`: the child, with the complete IAM root token inside its `fora_parent` claim. +{/* delegation-chain: docs_delegated_sub_agent */} {/* fora-validate: ResourceQuery */} ```json { @@ -710,9 +822,8 @@ The Kirkland legal agent from the previous example delegates to a sub-agent that "principal_domain": "iam.kirkland.com", "principal_id": "user@kirkland.com", "scopes": ["credit:read"], - "expires_at": "2026-03-20T19:00:00Z", - "max_spend_cents": 20000, - "token": "ZXlKaGJHY2lPaUpGWkVSVFFTSjkuPGNoaWxkIGNsYWltczogaXNzLCBzY29wZSwgZXhwLCBjbmYuamt0Pi48c2lnPg", + "expires_at": "2026-06-01T12:30:00Z", + "token": "ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYW5kcklqcDdJbU55ZGlJNklrVmtNalUxTVRraUxDSnJkSGtpT2lKUFMxQWlMQ0o0SWpvaVpHdFZlVkpXTm10dE4xcERWa3BaV1dac1VUTXdNWEZaY21KNk5ESnZaR2h5WWxWU1dIRmtiVlJ6T0NKOUxDSjBlWEFpT2lKbWIzSmhMV1JsYkdWbllYUnBiMjRyYW5kMEluMC5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVaWGhoYlhCc1pTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaVZtbGlSak15UWtNMVQyVnlSSFl0U2pOdGNERllRM1YzYnprM1dGSkRTaTFGZFZOS2JHeEtTMEp4VlNKOUxDSmxlSEFpT2pFM09EQXpNVGN3TURBc0ltWnZjbUZmY0dGeVpXNTBJam9pWlhsS2FHSkhZMmxQYVVwR1drVlNWRkZUU1hOSmJsSTFZME5KTmtsdFduWmpiVVYwV2tkV2MxcFhaR2hrUjJ4MlltbDBjV1F6VVdsbVVTNWxlVXBvWkZkUmFVOXNjMmxhV0docVlVZEdkVm95VlhWYVdHaG9ZbGhDYzFwVFNtUk1RMHBxWW0xWmFVOXVjMmxoYlhRd1NXcHZhVkp0Vm0xVGEzTjVWREIwWVZOdFNrTlZiR3hxWlcxa1RVOUliRnBoZWxKeVZYcFdWVk5HT1ZWVmJXOTRaRE5KTTFJeFNubGhWRTE1VGtOS09VeERTbXhsU0VGcFQycEZNMDlFUVhwTlZHYzBUVVJCYzBsdGJIcGplVWsyU1cxc2FHSlROWEpoV0VweVlrZEdkVnBETldwaU1qQnBURU5LY1dSSGEybFBhVXB3V1Zjd2RFNHlUWGhhVTBselNXNU9hbUl6UW14SmFtOXBZa2RXYmxsWGR6WlpNa1o2V2xNeGMxbFlZMmRpUjFadVdWZDNObU16VW1oa1NGWXdXbGhOWjFrelNteGFSMnd3VDI1S2JGbFhVV2xNUTBwNlpGZEphVTlwU2pGak1sWjVVVWQwY0dOdGRITlpWelZyVEcxT2RtSlRTamt1Y2padVlYaEthMDh0VDJGRFUwY3piRU5uWVhOR1JYVkZVRGt0YkhnM1VGTkRWVTF5ZEcxcGExQTVZUzFyYmtWbmRUbHlSMUZ3WWs5a1ZUZFBiV2xwYzJKa2IxUllUMWczU0VoNGRteHNOMXB6ZEZSTlExRWlMQ0pwYzNNaU9pSkdaV1pLU3pKUFMxcEtZa0pTV1dONlowdzRlVmxyTkd0VE5WUklYMVJTYWpGM2NqZEhVbkpwTXpJMElpd2lhblJwSWpvaWJHVm5ZV3d0YzNWaUxUQXdNRGNpTENKelkyOXdaU0k2SW1OeVpXUnBkRHB5WldGa0luMC5jYkFYNlo2bi03aVVHNEFhQTQ5a1NRVzR4bi1MaWdVUWN3R3RRaVRFT2VDQjBhRlVsUHZOdmloUDZYTnBwVVo4OThuMXNOMmhJQ29mY3NtMkJ3dFhCUQ==", "token_format": "jwt" } }, @@ -727,13 +838,12 @@ The Kirkland legal agent from the previous example delegates to a sub-agent that Key differences from the parent agent's request: - `type` is `REQUESTER_TYPE_DELEGATED` — signals this agent acts on behalf of another -- `delegation.scopes` is narrowed to `["credit:read"]` (legal scopes removed) -- `delegation.max_spend_cents` is reduced from `100000` ($1,000) to `20000` ($200) -- `delegation.expires_at` is shortened (1 hour earlier) -- The sub-agent sends and signs the request alone, with its own labeled RFC 9421 signature and its own `Signature-Agent` member (see [Multi-hop forwarding](#multi-hop-forwarding)). The parent's authority does not travel as a request signature: it travels in the delegation's JWT chain -- The delegation is a JWT chain: the authority JWT (signed by IAM) plus a child JWT (signed by the parent agent) whose `cnf.jkt` binds the grant to the sub-agent's key and whose `scope` is narrowed +- `delegation.scopes` is narrowed to `["credit:read"]` (legal scopes removed); the mirrors `principal_domain` and `principal_id` still describe the root, because they mirror the root token's `iss` +- `delegation.expires_at` is shortened (30 minutes earlier), the child's own `exp` +- The sub-agent sends and signs the request alone, with its own labeled RFC 9421 signature and its own `Signature-Agent` member (see [Multi-hop forwarding](#multi-hop-forwarding)). The parent's authority does not travel as a request signature: it travels inside the delegation token +- The delegation is a nested chain: the child token, signed by the parent agent with `alg: Ed25519`, carries the complete IAM root token in `fora_parent`, the parent agent's public key in its protected `jwk` header, that key's thumbprint as its `iss`, and the sub-agent's key thumbprint as its `cnf.jkt` -The Exchange verifies the full JWT chain: IAM signed the authority JWT; the parent agent signed the child JWT (its signing key matches the authority's `cnf` — chain linkage); the sub-agent's request key matches the child's `cnf.jkt`. No call to IAM was required for the delegation. +The Exchange verifies the full chain: IAM signed the root token, checked with the configured key; the parent agent signed the child (its `jwk` hashes to the root's `cnf.jkt` — chain linkage); the sub-agent's request key matches the child's `cnf.jkt`. No call to IAM was required for the delegation. ## Key Management @@ -821,7 +931,7 @@ Key rotation follows a standard overlap pattern: 3. After a grace period (Exchange-defined, typically 24-48 hours), remove the old key 4. Exchanges SHOULD cache keys with a TTL and re-fetch periodically -For delegation JWTs, key rotation at the issuer does not invalidate existing tokens — the authority JWT was signed with the key that was current at issuance time. Exchanges SHOULD accept tokens signed by recently-rotated keys within a configurable grace period. +For delegation tokens, an issuer's keys are configured on the subscription record, not published in a directory, so rotation is an edit to that list: the new key is added, and the old key stays listed until every token it signed has expired, then it is removed. A root token verifies with any listed key whose identity equals its `iss`, so rotation does not invalidate existing tokens, and removing a key is the explicit withdrawal of trust from everything it signed. ### Key Validity Windows diff --git a/website/src/content/docs/protocol/walkthrough-academic.mdx b/website/src/content/docs/protocol/walkthrough-academic.mdx index e397e089..59358ac6 100644 --- a/website/src/content/docs/protocol/walkthrough-academic.mdx +++ b/website/src/content/docs/protocol/walkthrough-academic.mdx @@ -13,15 +13,16 @@ A university AI research agent needs 50 academic papers for a literature review |---|---|---|---| | **arXiv** | Open Access (Gold) | 12 | Free | | **University subscription** | Institutional deal via Elsevier | 20 | $0.00 (subscription) | -| **Elsevier** (no subscription) | Paywalled | 17 | $1.50/article | +| **Elsevier** (outside the subscription) | Paywalled | 2 | $1.50/article | +| **Springer Nature** | Paywalled | 15 | $1.50/article | | **Springer Nature** | Paywalled, retracted | 1 | $1.50/article (flagged) | -The agent carries a delegation JWT from the university, granting `academic:*` scope, and states the university's subscription scope in its requests. The Broker resolves DOIs via OpenAlex, discovers Exchanges via `fora.json`, and uses the `fora-academic-v1` extension profile to access version, OA status, and retraction metadata. +The agent carries a delegation JWT from the university, granting `academic:*` and the subscription scope `subscription:inst-sub-stanford-2026`. The subscription term is gated behind that delegation, and the Exchange honors a delegation only on a request the agent signs itself, so the agent queries the Exchange directly for the papers the Broker found only at a price. The Broker resolves DOIs via OpenAlex, discovers Exchanges via `fora.json`, and uses the `fora-academic-v1` extension profile to access version, OA status, and retraction metadata. -**Six-step flow:** +**Seven-step flow:** ``` -Discovery → ResourceQuery → ResourceResponse → Broker Selection → Batch ExecuteTransaction → ReportUsage +Discovery → ResourceQuery (Broker-led) → ResourceResponse → Direct ResourceQuery with the delegation → Broker Selection → Batch ExecuteTransaction → ReportUsage ``` --- @@ -94,12 +95,13 @@ All three publishers route through the same academic aggregator Exchange (`excha --- -## Step 2 — ResourceQuery +## Step 2 — ResourceQuery (Broker-led) -The Broker sends a batch `ResourceQuery` with all 50 DOIs in `ResourceQuery.uris`. The `Requester` states the agent's scopes, the university's subscription scope among them, and carries the university's delegation JWT, which the Exchange does not honour on a query the Broker originated (see below). The query declares support for `fora-academic-v1` so the Exchange includes version, OA status, and retraction metadata in its response. +The Broker sends a batch `ResourceQuery` with all 50 DOIs in `ResourceQuery.uris`. The `Requester` states the agent's scopes, the university's subscription scope among them, and carries the university's delegation token (the shared chain case `docs_walkthrough_academic`), which the Exchange does not honor on a query the Broker originated (see below). The query declares support for `fora-academic-v1` so the Exchange includes version, OA status, and retraction metadata in its response. `POST https://exchange.scholarsgate.com/fora/v1/fora.v1.ExchangeService/DiscoverResources` +{/* delegation-chain: docs_walkthrough_academic */} {/* fora-validate: ResourceQuery */} ```json { @@ -114,11 +116,9 @@ The Broker sends a batch `ResourceQuery` with all 50 DOIs in `ResourceQuery.uris "delegation": { "principal_domain": "stanford.edu", "principal_id": "inst-sub-stanford-2026", - "scopes": ["academic:*"], + "scopes": ["academic:*", "subscription:inst-sub-stanford-2026"], "expires_at": "2026-12-31T23:59:59Z", - "max_accesses": 100000, - "quota_period": "2592000s", - "token": "ZXlKaGJHY2lPaUpGWkVSVFFTSjkuPGNsYWltczogaXNzLCBzY29wZSwgZXhwLCBjbmYuamt0Pi48c2lnPg", + "token": "ZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNkltWnZjbUV0WkdWc1pXZGhkR2x2Yml0cWQzUWlmUS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVjMk5vYjJ4aGNuTm5ZWFJsTG1OdmJTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaVFWTk9hVUZGVTNOUWNYVTNaRmhOTlRoTU5rTmhiMHRzVEdSRlZWaE1WRzVpVUVwcmFFaHBhbDk1UlNKOUxDSmxlSEFpT2pFM09UZzNOakUxT1Rrc0ltbHpjeUk2SW5OMFlXNW1iM0prTG1Wa2RTSXNJbXAwYVNJNkltbHVjM1F0YzNWaUxYTjBZVzVtYjNKa0xUSXdNall0TURFNU15SXNJbk5qYjNCbElqb2lZV05oWkdWdGFXTTZLaUJ6ZFdKelkzSnBjSFJwYjI0NmFXNXpkQzF6ZFdJdGMzUmhibVp2Y21RdE1qQXlOaUlzSW5OMVlpSTZJbWx1YzNRdGMzVmlMWE4wWVc1bWIzSmtMVEl3TWpZaWZRLnExb3RueTVnM092WWxnOHpyUUU2aUNZZFd4eEltT0pmNjRyMDRyNVowUGFZWTY5blRVWF9NWHpCcUZmcDVIWjRaTTQ1bWt1eDZBY2RWeVJVbHJQX0N3", "token_format": "jwt" } }, @@ -182,27 +182,31 @@ The Broker sends a batch `ResourceQuery` with all 50 DOIs in `ResourceQuery.uris **Key fields in this request:** - **`ResourceQuery.uris`** — 50 DOIs in `doi:` URI scheme. The Exchange resolves these against its catalog. DOIs are the universal dedup key for academic content: the same paper on arXiv and Elsevier shares the same DOI (or is linked via `academic.related_works`). -- **`requester.scopes`** — the agent's own statement: `academic:*` for full academic access, and `subscription:inst-sub-stanford-2026` for the university's subscription. The Exchange does not verify them. It presents each term whose scopes they cover, so the subscription term, which carries `subscription:inst-sub-stanford-2026`, is presented. -- **`delegation`** — A delegation JWT signed by `stanford.edu`'s Ed25519 key. Its claims contain: +- **`requester.scopes`** — the agent's own statement: `academic:*` for full academic access, and `subscription:inst-sub-stanford-2026` for the university's subscription. The Exchange does not verify them, and they decide only which non-gated terms are presented. The subscription term is gated behind a verified delegation, so stating its scope does not present it ([Scope-Based Access](/protocol/authentication/#scope-based-access)). +- **`delegation`** — A root token in the [delegation profile](/protocol/authentication/#delegation), signed by `stanford.edu`'s Ed25519 key. Decoded, its header and claims contain: ```json + // protected header + { "alg": "EdDSA", "typ": "fora-delegation+jwt" } + // claims { "iss": "stanford.edu", "sub": "inst-sub-stanford-2026", - "scope": "academic:*", - "fora_max_accesses": 100000, - "fora_quota_period": "2592000s", + "aud": ["exchange.scholarsgate.com"], + "jti": "inst-sub-stanford-2026-0193", + "scope": "academic:* subscription:inst-sub-stanford-2026", "exp": 1798761599, - "cnf": { "jkt": "" } + "cnf": { "jkt": "ASNiAESsPqu7dXM58L6CaoKlLdEUXLTnbPJkhHij_yE" } } ``` - On this query the Exchange does not honour the delegation: the Broker originated the query and signs it alone, so nothing on it can pass the holder binding, and the Exchange answers as if no delegation were present ([Which signature binds in a brokered request](/protocol/authentication/#verification)). The agent's direct purchase in Step 5 verifies it instead, offline and with no call to Stanford. An Exchange that requires a verified delegation before it presents a scoped term would omit the subscription term from this answer, as if it did not exist; the agent would then query the Exchange directly, signing the query itself, with a delegation that grants the subscription scope. + The monthly access quota is not a token claim: the Exchange meters it on the subscription record and reports it in `subscription_quota`. + On this query the Exchange does not honor the delegation: the Broker originated the query and signs it alone, so nothing on it can pass the holder binding, and the Exchange answers as if no delegation were present ([Which signature binds in a brokered request](/protocol/authentication/#verification)). The subscription term is therefore hidden from this answer, as if it did not exist. In Step 4 the agent queries the Exchange directly, signing the query itself, and the delegation is verified there, offline and with no call to Stanford. - **`supported_profiles`** — `["fora-academic-v1"]` tells the Exchange to include academic-specific `ext` fields (OA status, version, retraction status) and attestation claims. Without this declaration, the Exchange MAY skip expensive metadata computation like retraction checking. --- ## Step 3 — ResourceResponse -The Exchange resolves all 50 DOIs against its catalog, presents the terms the stated scopes cover, the institutional subscription term among them, and returns 50 `OfferGroup` entries. Below are representative examples from each category — the full response contains all 50 groups. +The Exchange resolves all 50 DOIs against its catalog, presents the non-gated terms the stated scopes cover, and returns 50 `OfferGroup` entries. The institutional subscription term is gated behind a verified delegation, and the Exchange honors none on this query, so the 20 Elsevier papers under Stanford's subscription come back with their paid offer, exactly as for a requester with no subscription. Below are representative examples from each category — the full response contains all 50 groups. ```json { @@ -288,18 +292,17 @@ The Exchange resolves all 50 DOIs against its catalog, presents the terms the st "uri": "doi:10.1016/j.media.2024.103250", "offers": [ { - "offer_id": "ba9b114a-1e5b-473c-886c-4b9ac9b89a9d", + "offer_id": "5d1f3c0a-8b64-4e2f-9a7d-2c6e41b0f9e3", "exchange": "exchange.scholarsgate.com", "title": "TransUNet++: Rethinking Multi-Scale Feature Fusion for Medical Image Segmentation", "pricing": { - "model": "PRICING_MODEL_FREE", - "rate": "0", + "model": "PRICING_MODEL_PER_UNIT", + "rate": "1.50", "currency": "USD", - "unit_cost": "0", - "estimated_quantity": 15200, - "unit": "tokens" + "unit_cost": "0.0000987", + "estimated_quantity": 1, + "unit": "accesses" }, - "subscription_id": "inst-sub-stanford-2026", "delivery_method": "DELIVERY_METHOD_INSTRUCTIONS", "reporting": { "required": true, @@ -340,21 +343,10 @@ The Exchange resolves all 50 DOIs against its catalog, presents the terms the st "permitted": ["ai-input", "search"], "prohibited": ["ai-train"] } - ], - "scopes": ["subscription:inst-sub-stanford-2026"] - } - ], - "subscription_quota": [ - { - "subscription_id": "inst-sub-stanford-2026", - "quota_limit": 100000, - "quota_used": 23480, - "quota_remaining": 76520, - "resets_at": "2026-04-01T00:00:00Z", - "unit": "accesses" + ] } ], - "signature": "base64-ed25519-elsevier-sub-offer-sig...", + "signature": "base64-ed25519-elsevier-paid-offer-sig...", "signature_algorithm": "EdDSA", "ext": { "comp.package_id": "PKG-ELSEVIER-MEDIA-103250", @@ -538,13 +530,12 @@ The Exchange resolves all 50 DOIs against its catalog, presents the terms the st ### Offer Categories -The Exchange classified each of the 50 DOIs into one of four categories based on the publisher's access model and the scopes the requester stated: +The Exchange classified each of the 50 DOIs into one of three categories based on the publisher's access model; the subscription term cannot appear on this query: | Category | Count | Pricing Model | Rate | Key Signal | |---|---|---|---|---| | **Free OA (arXiv)** | 12 | `PRICING_MODEL_FREE` | $0.00 | `academic.oa_status: "gold"`, `academic.version: "preprint"` | -| **Subscription (Elsevier)** | 20 | subscription (FREE + subscription scope) | $0.00 | `subscription_id: "inst-sub-stanford-2026"` | -| **Paid (Springer + Elsevier)** | 17 | `PRICING_MODEL_PER_UNIT` | $1.50 | No subscription match, `academic.oa_status: "closed"` | +| **Paid (Elsevier + Springer)** | 37 | `PRICING_MODEL_PER_UNIT` | $1.50 | `academic.oa_status: "closed"`; 20 of them are under Stanford's subscription, which this query cannot show | | **Retracted (Springer)** | 1 | `PRICING_MODEL_PER_UNIT` | $1.50 | `academic.retraction_status: "retracted"` in `ext_critical` | ### Extension Profile Fields (`fora-academic-v1`) @@ -572,25 +563,199 @@ Every offer carries a `ResourceIdentity` with: - **`content_hash`** — SHA-256 hash for post-delivery verification. Because `resource_mutability` is `RESOURCE_MUTABILITY_STATIC` for all academic papers (published articles do not change — corrections and retractions create new resources), the agent SHOULD verify the hash on delivery. - **`canonical_url`** — The publisher's authoritative URL. Different from the DOI URI but resolves to the same resource. +## Step 4 — Direct ResourceQuery with the delegation + +The Broker's answer prices 37 papers at $1.50, and Stanford's subscription covers 20 of them. The Exchange presents the subscription term only to a requester whose delegation it has verified, and verification needs the agent's own signature on the request. The agent therefore sends a second `ResourceQuery` directly to ScholarsGate, signed with its own key and carrying the same delegation, for the 37 DOIs the Broker found only at a price. + +`POST https://exchange.scholarsgate.com/fora/v1/fora.v1.ExchangeService/DiscoverResources` + +{/* delegation-chain: docs_walkthrough_academic */} +{/* fora-validate: ResourceQuery */} +```json +{ + "ver": "1.0", + "exchange": "exchange.scholarsgate.com", + "requester": { + "id": "litrev-agent", + "domain": "cs.stanford.edu", + "type": "REQUESTER_TYPE_AGENT", + "name": "Stanford CS Literature Review Agent", + "scopes": ["academic:*", "subscription:inst-sub-stanford-2026"], + "delegation": { + "principal_domain": "stanford.edu", + "principal_id": "inst-sub-stanford-2026", + "scopes": ["academic:*", "subscription:inst-sub-stanford-2026"], + "expires_at": "2026-12-31T23:59:59Z", + "token": "ZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNkltWnZjbUV0WkdWc1pXZGhkR2x2Yml0cWQzUWlmUS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVjMk5vYjJ4aGNuTm5ZWFJsTG1OdmJTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaVFWTk9hVUZGVTNOUWNYVTNaRmhOTlRoTU5rTmhiMHRzVEdSRlZWaE1WRzVpVUVwcmFFaHBhbDk1UlNKOUxDSmxlSEFpT2pFM09UZzNOakUxT1Rrc0ltbHpjeUk2SW5OMFlXNW1iM0prTG1Wa2RTSXNJbXAwYVNJNkltbHVjM1F0YzNWaUxYTjBZVzVtYjNKa0xUSXdNall0TURFNU15SXNJbk5qYjNCbElqb2lZV05oWkdWdGFXTTZLaUJ6ZFdKelkzSnBjSFJwYjI0NmFXNXpkQzF6ZFdJdGMzUmhibVp2Y21RdE1qQXlOaUlzSW5OMVlpSTZJbWx1YzNRdGMzVmlMWE4wWVc1bWIzSmtMVEl3TWpZaWZRLnExb3RueTVnM092WWxnOHpyUUU2aUNZZFd4eEltT0pmNjRyMDRyNVowUGFZWTY5blRVWF9NWHpCcUZmcDVIWjRaTTQ1bWt1eDZBY2RWeVJVbHJQX0N3", + "token_format": "jwt" + } + }, + "uris": [ + "doi:10.1016/j.media.2024.103250", + "doi:10.1016/j.media.2024.103178", + "doi:10.1016/j.media.2024.103095", + "doi:10.1016/j.media.2023.103012", + "doi:10.1016/j.media.2023.102956", + "doi:10.1016/j.media.2023.102891", + "doi:10.1016/j.media.2023.102834", + "doi:10.1016/j.media.2023.102770", + "doi:10.1016/j.media.2022.102689", + "doi:10.1016/j.media.2022.102615", + "doi:10.1016/j.media.2022.102542", + "doi:10.1016/j.media.2022.102478", + "doi:10.1016/j.media.2022.102410", + "doi:10.1016/j.media.2021.102345", + "doi:10.1016/j.media.2021.102287", + "doi:10.1016/j.media.2021.102219", + "doi:10.1016/j.media.2021.102154", + "doi:10.1038/s41592-024-02401-5", + "doi:10.1038/s41592-024-02318-0", + "doi:10.1038/s41592-024-02245-7", + "doi:10.1038/s41592-023-02112-4", + "doi:10.1038/s41592-023-02038-1", + "doi:10.1038/s41592-023-01975-3", + "doi:10.1038/s41592-023-01901-x", + "doi:10.1038/s41592-023-01847-2", + "doi:10.1038/s41592-022-01789-5", + "doi:10.1038/s41592-022-01714-6", + "doi:10.1038/s41592-022-01648-3", + "doi:10.1038/s41592-022-01581-5", + "doi:10.1038/s41592-022-01517-1", + "doi:10.1038/s41592-021-01389-9", + "doi:10.1038/s41592-021-01312-y", + "doi:10.1016/j.media.2024.103301", + "doi:10.1016/j.media.2024.103267", + "doi:10.1016/j.media.2023.103189", + "doi:10.1016/j.media.2023.103145", + "doi:10.1016/j.media.2023.103078" + ], + "acceptable_restrictions": [{ "axis": "RESTRICTION_KIND_FUNCTION", "values": ["ai-input"] }], + "supported_profiles": ["fora-academic-v1"] +} +``` + +This request is direct, so the Exchange verifies the delegation as [Verification](/protocol/authentication/#verification) describes, with the key configured for `stanford.edu` on the subscription record. The key that signed the request (RFC 9421) hashes to the token's `cnf.jkt`, so the holder binding passes. The effective scopes the verifier returns, `academic:*` and `subscription:inst-sub-stanford-2026`, cover the subscription term's scope, so for each of the 20 covered DOIs the response carries the subscription offer next to the paid one from Step 3. The 17 papers outside the subscription come back exactly as in Step 3. One covered DOI: + +```json +{ + "ver": "1.0", + "exchange": "exchange.scholarsgate.com", + "offer_groups": [ + { + "uri": "doi:10.1016/j.media.2024.103250", + "offers": [ + { + "offer_id": "ba9b114a-1e5b-473c-886c-4b9ac9b89a9d", + "exchange": "exchange.scholarsgate.com", + "title": "TransUNet++: Rethinking Multi-Scale Feature Fusion for Medical Image Segmentation", + "pricing": { + "model": "PRICING_MODEL_FREE", + "rate": "0", + "currency": "USD", + "unit_cost": "0", + "estimated_quantity": 15200, + "unit": "tokens" + }, + "subscription_id": "inst-sub-stanford-2026", + "delivery_method": "DELIVERY_METHOD_INSTRUCTIONS", + "reporting": { + "required": true, + "window": "86400s", + "required_fields": ["consumed_quantity"] + }, + "identity": { + "canonical_url": "https://www.sciencedirect.com/science/article/pii/S1361841524001250", + "doi": "10.1016/j.media.2024.103250", + "content_hash": "sha256:b2c3d4e5f6a789012345678bcdefa0123456789abcdef0123456789abcdef02", + "hash_method": "sha256", + "resource_mutability": "RESOURCE_MUTABILITY_STATIC" + }, + "attestations": [ + { + "verifier": "elsevier.com", + "keyid": "kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k", + "attested_at": "2026-03-10T06:00:00Z", + "uri": "doi:10.1016/j.media.2024.103250", + "claims": { + "estimated_quantity": 15200, + "word_count": 11500, + "language": "en", + "academic.oa_status": "closed", + "academic.version": "vor", + "academic.retraction_status": "active", + "academic.peer_review_status": "peer_reviewed" + }, + "signature": "base64-ed25519-elsevier-self-attestation..." + } + ], + "terms": [ + { + "semantics": "TERM_SEMANTICS_ENUMERATED", + "restrictions": [ + { + "kind": "RESTRICTION_KIND_FUNCTION", + "permitted": ["ai-input", "search"], + "prohibited": ["ai-train"] + } + ], + "scopes": ["subscription:inst-sub-stanford-2026"] + } + ], + "subscription_quota": [ + { + "subscription_id": "inst-sub-stanford-2026", + "quota_limit": 100000, + "quota_used": 23480, + "quota_remaining": 76520, + "resets_at": "2026-04-01T00:00:00Z", + "unit": "accesses" + } + ], + "signature": "base64-ed25519-elsevier-sub-offer-sig...", + "signature_algorithm": "EdDSA", + "ext": { + "comp.package_id": "PKG-ELSEVIER-MEDIA-103250", + "comp.seller": "elsevier.com", + "comp.citation_required": true, + "comp.retrieval_type": ["RETRIEVAL_TYPE_PDF", "RETRIEVAL_TYPE_XML"], + "academic.oa_status": "closed", + "academic.version": "vor", + "academic.journal": "Medical Image Analysis", + "academic.issn": "1361-8415", + "academic.publisher": "Elsevier" + } + } + ] + } + ], + "rate_limit": { + "limit": 100, + "remaining": 93, + "reset_at": "2026-03-20T13:00:00Z", + "window": "3600s" + } +} +``` + ### Subscription Detection For the 20 Elsevier papers covered by Stanford's institutional subscription: -- subscription (FREE + subscription scope): `pricing.model` is `PRICING_MODEL_FREE` with `rate: 0`, and the term carries `scopes: ["subscription:inst-sub-stanford-2026"]` +- the subscription offer is `PRICING_MODEL_FREE` with `rate: 0`, and its term carries `scopes: ["subscription:inst-sub-stanford-2026"]` - `subscription_id` matches the delegation JWT's `principal_id` (`inst-sub-stanford-2026`) - `subscription_quota` shows remaining capacity (76,520 of 100,000 accesses this period) -- The Exchange presented the term on the stated scope alone; the agent's direct purchase in Step 5 verifies the delegation's JWT chain offline, with no call to Stanford's identity provider +- the Exchange presented the term because the verified delegation's effective scopes cover it; the stated `requester.scopes` play no part for a gated term, and the delegation was verified offline, with no call to Stanford's identity provider --- -## Step 4 — Broker Selection +## Step 5 — Broker Selection -The Broker applies a cost-minimization strategy across all 50 OfferGroups: +The agent hands the direct answer from Step 4 to the Broker, which applies a cost-minimization strategy across all 50 OfferGroups, with the 20 subscription offers in place of the paid Elsevier offers for those DOIs: ### Selection Rules 1. **Exclude retracted papers** — The retracted paper is flagged to the research agent. The agent decides whether to exclude it from the review or include it with a retraction annotation. 2. **Prefer free OA** — 12 arXiv papers at $0.00 each. -3. **Prefer subscription** — 20 Elsevier papers at $0.00 each (institutional deal). +3. **Prefer subscription** — 20 Elsevier papers at $0.00 each (the subscription offers from Step 4). 4. **Cheapest paid for the rest** — 17 papers at $1.50 each from Springer and Elsevier (no subscription coverage). ### Cost Summary @@ -611,12 +776,13 @@ The Broker also flags the retracted paper to the agent: --- -## Step 5 — Batch ExecuteTransaction +## Step 6 — Batch ExecuteTransaction The research agent commits to all 49 selected offers in a single batch `TransactionRequest`, sent directly to the Exchange and signed by the agent: every selected offer is from ScholarsGate. Through the Broker it would send the same request, with each item's `agent_acceptance` and the request's `agent_request_acceptance`, to `BrokerService.ExecuteTransaction`, and the Broker would re-package it for the Exchange and sign that request alone (see [Buying through a Broker](/protocol/transaction-flow/#buying-through-a-broker)). Each item carries the full signed `offer`, reflected back exactly as received at discovery (stateless verification — the Exchange does not need to store offers). `POST https://exchange.scholarsgate.com/fora/v1/fora.v1.ExchangeService/ExecuteTransaction` +{/* delegation-chain: docs_walkthrough_academic */} {/* fora-validate: TransactionRequest */} ```json { @@ -630,11 +796,9 @@ The research agent commits to all 49 selected offers in a single batch `Transact "delegation": { "principal_domain": "stanford.edu", "principal_id": "inst-sub-stanford-2026", - "scopes": ["academic:*"], + "scopes": ["academic:*", "subscription:inst-sub-stanford-2026"], "expires_at": "2026-12-31T23:59:59Z", - "max_accesses": 100000, - "quota_period": "2592000s", - "token": "ZXlKaGJHY2lPaUpGWkVSVFFTSjkuPGNsYWltczogaXNzLCBzY29wZSwgZXhwLCBjbmYuamt0Pi48c2lnPg", + "token": "ZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNkltWnZjbUV0WkdWc1pXZGhkR2x2Yml0cWQzUWlmUS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXVjMk5vYjJ4aGNuTm5ZWFJsTG1OdmJTSmRMQ0pqYm1ZaU9uc2lhbXQwSWpvaVFWTk9hVUZGVTNOUWNYVTNaRmhOTlRoTU5rTmhiMHRzVEdSRlZWaE1WRzVpVUVwcmFFaHBhbDk1UlNKOUxDSmxlSEFpT2pFM09UZzNOakUxT1Rrc0ltbHpjeUk2SW5OMFlXNW1iM0prTG1Wa2RTSXNJbXAwYVNJNkltbHVjM1F0YzNWaUxYTjBZVzVtYjNKa0xUSXdNall0TURFNU15SXNJbk5qYjNCbElqb2lZV05oWkdWdGFXTTZLaUJ6ZFdKelkzSnBjSFJwYjI0NmFXNXpkQzF6ZFdJdGMzUmhibVp2Y21RdE1qQXlOaUlzSW5OMVlpSTZJbWx1YzNRdGMzVmlMWE4wWVc1bWIzSmtMVEl3TWpZaWZRLnExb3RueTVnM092WWxnOHpyUUU2aUNZZFd4eEltT0pmNjRyMDRyNVowUGFZWTY5blRVWF9NWHpCcUZmcDVIWjRaTTQ1bWt1eDZBY2RWeVJVbHJQX0N3", "token_format": "jwt" } }, @@ -706,15 +870,15 @@ The research agent commits to all 49 selected offers in a single batch `Transact } ``` -*(Abbreviated: 2 of 49 items shown. The remaining 47 follow the same shape — each carries the full signed Offer reflected back: 12 arXiv (free), 18 more Elsevier (subscription), 15 Springer + 2 Elsevier (paid).)* +*(Abbreviated: 2 of 49 items shown. The remaining 47 follow the same shape — each carries the full signed Offer reflected back: 11 more arXiv (free), 19 more Elsevier (subscription), 15 Springer + 2 Elsevier (paid).)* The Exchange processes all 49 items in a single batch: 1. **Signature verification** — verifies each item's `offer.signature` is its own Ed25519 signature over the reflected Offer (stateless, no stored offers). 2. **Agent authentication** — verifies the request's RFC 9421 HTTP Message Signature (alg=ed25519, carried in the HTTP headers) against the agent's public key published in the WBA directory at `cs.stanford.edu/.well-known/http-message-signatures-directory`. -3. **Delegation verification** — verifies the JWT chain for the 20 subscription items. Confirms `academic:*` scope covers Elsevier content. +3. **Delegation verification** — verifies the chain again for the 20 subscription items, as in Step 4: the key configured for `stanford.edu` on the subscription record, `aud` naming this Exchange, `cnf.jkt` equal to the agent's request key, and effective scopes that cover the subscription term. For this gated term the stated scopes are ignored. 4. **Balance check** — confirms the account of the `cs.stanford.edu` key directory has sufficient balance ($25.50 for 17 paid items); `litrev-agent` is only a label and selects no account. -5. **Quota check** — confirms 49 accesses fit within the subscription's remaining quota (76,520). +5. **Quota check** — confirms the 20 subscription accesses fit within the subscription's remaining quota (76,520). 6. **WAL write** — records all 49 transactions in the write-ahead log before generating signed URLs (write-before-sign invariant). 7. **Signed URL generation** — generates 49 Ed25519 signed URLs, each binding `agent_id`. @@ -810,7 +974,7 @@ The agent then fetches all 49 PDFs via the signed URLs. For each paper, because --- -## Step 6 — ReportUsage +## Step 7 — ReportUsage Usage reporting is mandatory for all 49 transactions. A usage report settles one transaction, so the agent files one report per transaction: 49 reports. Each report states consumption in the unit of that item's offer. The 12 arXiv and 20 Elsevier subscription offers are priced in `tokens`, so their reports carry token counts. The 17 paid offers are priced per access, so each of their reports carries one access. Here is the report for the paid CellViT paper: @@ -864,15 +1028,16 @@ Response: | Step | RPC | Free OA (12) | Subscription (20) | Paid (17) | Retracted (1) | |------|-----|---|---|---|---| | 1 | -- | `fora.json` discovery | `fora.json` discovery | `fora.json` discovery | `fora.json` discovery | -| 2 | `DiscoverResources` (Broker-led) | `Requester` stating `academic:*` | Stated subscription scope covers the term; delegation JWT not honoured | Same | Same | -| 3 | -- | Offers at $0, `oa_status: "gold"` | Offers at $0, `subscription_id` set | Offers at $1.50 | Offer with `retraction_status: "retracted"` in `ext_critical` | -| 4 | -- | Broker selects (free) | Broker selects (free via sub) | Broker selects (cheapest) | Broker flags and excludes | -| 5 | `ExecuteTransaction` | Batch, cost $0 | Batch, cost $0, sub quota decremented | Batch, cost $1.50/ea | Not transacted | -| 6 | `ReportUsage` | One report per transaction, in tokens | One report per transaction, in tokens | One report per transaction, 1 access each | No report | +| 2 | `DiscoverResources` (Broker-led) | `Requester` stating `academic:*` | Subscription term hidden: the delegation is not honored on the Broker's query | Same | Same | +| 3 | -- | Offers at $0, `oa_status: "gold"` | Paid offers at $1.50 only | Offers at $1.50 | Offer with `retraction_status: "retracted"` in `ext_critical` | +| 4 | `DiscoverResources` (direct, with the delegation) | -- | Delegation verified; offers at $0, `subscription_id` set | Unchanged, $1.50 | -- | +| 5 | -- | Broker selects (free) | Broker selects (free via sub) | Broker selects (cheapest) | Broker flags and excludes | +| 6 | `ExecuteTransaction` | Batch, cost $0 | Batch, cost $0, sub quota decremented | Batch, cost $1.50/ea | Not transacted | +| 7 | `ReportUsage` | One report per transaction, in tokens | One report per transaction, in tokens | One report per transaction, 1 access each | No report | **Authentication at every step:** - **Agent to Exchange**: signed with an RFC 9421 HTTP Message Signature (alg=ed25519) in the HTTP headers -- **Institutional delegation**: delegation JWT signed by `stanford.edu`'s Ed25519 key, holder-of-key bound via `cnf.jkt` (offline-verifiable), verified on the agent's direct purchase and not honoured on the Broker's query +- **Institutional delegation**: delegation JWT signed by `stanford.edu`'s Ed25519 key, holder-of-key bound via `cnf.jkt` (offline-verifiable), verified on the agent's direct query and direct purchase, not honored on the Broker's query - **Exchange to Agent**: `exchange_signature` on each Offer (Ed25519, stateless verification) - **Signed URLs**: Ed25519 (Exchange-signed, publicly verifiable, agent identity bound) - **Content integrity**: SHA-256 hash verification on all 49 delivered PDFs (`RESOURCE_MUTABILITY_STATIC`) @@ -880,7 +1045,7 @@ Response: **Key academic-specific capabilities demonstrated:** - **DOI as universal dedup key** -- cross-exchange resource identification - **Mixed pricing in single batch** -- FREE + subscription (FREE + subscription scope) + PER_UNIT in one transaction -- **Institutional subscription** -- presented on the stated subscription scope; the delegation JWT is verified offline at purchase, no call to Stanford +- **Institutional subscription** -- hidden on the Broker's query, presented on the agent's direct query once the delegation JWT is verified offline, no call to Stanford - **Retraction flagging via `ext_critical`** -- consumers MUST understand retraction semantics or reject - **`fora-academic-v1` profile** -- OA status, version lifecycle, peer review status, retraction metadata - **Aggregator as Exchange** -- one Exchange serves content from multiple publishers diff --git a/website/src/content/docs/protocol/walkthrough-v1.mdx b/website/src/content/docs/protocol/walkthrough-v1.mdx index ca6a8ec9..0972afd7 100644 --- a/website/src/content/docs/protocol/walkthrough-v1.mdx +++ b/website/src/content/docs/protocol/walkthrough-v1.mdx @@ -361,10 +361,11 @@ curl -s https://marketdata.example.com/.well-known/fora.json | jq ### Step 2 — ResourceQuery with Delegation (JWT) -Here is where v1.0's `Requester` + `Delegation` model shows its power. The agent states its subscription scope and includes a delegation JWT that grants it: +Here is where v1.0's `Requester` + `Delegation` model shows its power. The agent states its subscription scope and includes a delegation token that grants it. The token is a real one from the shared chain cases (`docs_walkthrough_v1`), addressed to `exchange.marketdata-data.com` and bound to the agent's request key: `POST https://exchange.marketdata-data.com/fora/v1/fora.v1.ExchangeService/DiscoverResources` +{/* delegation-chain: docs_walkthrough_v1 */} {/* fora-validate: ResourceQuery */} ```json { @@ -386,7 +387,7 @@ Here is where v1.0's `Requester` + `Delegation` model shows its power. The agent "principal_id": "enterprise-sub-HF-001", "scopes": ["quote:*", "earnings:*", "news:read", "subscription:enterprise-sub-HF-001"], "expires_at": "2026-12-31T23:59:59Z", - "token": "ZXlKaGJHY2lPaUpGWkVSVFFTSjkuPGNsYWltczogaXNzLCBzY29wZSwgZXhwLCBjbmYuamt0Pi48c2lnPg", + "token": "ZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNkltWnZjbUV0WkdWc1pXZGhkR2x2Yml0cWQzUWlmUS5leUpoZFdRaU9sc2laWGhqYUdGdVoyVXViV0Z5YTJWMFpHRjBZUzFrWVhSaExtTnZiU0pkTENKamJtWWlPbnNpYW10MElqb2lVMlpqZVdaVVFucFVVbWhTYnkxbE1WQmxSQzFwVEMxSlgxZ3RVVVJzWjBGS1ozWlZlVFpPTjI5RFJTSjlMQ0psZUhBaU9qRTNPVGczTmpFMU9Ua3NJbWx6Y3lJNkltMWhjbXRsZEdSaGRHRXVaWGhoYlhCc1pTNWpiMjBpTENKcWRHa2lPaUp0WkMxb1ppMHdNREV0TWpBeU5pSXNJbk5qYjNCbElqb2ljWFZ2ZEdVNktpQmxZWEp1YVc1bmN6b3FJRzVsZDNNNmNtVmhaQ0J6ZFdKelkzSnBjSFJwYjI0NlpXNTBaWEp3Y21selpTMXpkV0l0U0VZdE1EQXhJaXdpYzNWaUlqb2laVzUwWlhKd2NtbHpaUzF6ZFdJdFNFWXRNREF4SW4wLklfdmZQS1ZWSEowYVFGSnhqcWd5WEd5MS0tMHE4NlA3Z2VmMldTT3RpWHZDWmtfeXNrWHVGLVhUeTB0VG43alQ3dm1MWUNFZTBpTlFKYkkxb1I4YkR3", "token_format": "jwt" } }, @@ -396,35 +397,44 @@ Here is where v1.0's `Requester` + `Delegation` model shows its power. The agent **What happens with the delegation token (JWT):** -The `delegation.token` is an authority JWT, signed by MarketData's Ed25519 key. Its claims carry the grant: +The `delegation.token` is a root token in the [delegation profile](/protocol/authentication/#delegation), signed by MarketData's Ed25519 key. Decoded, its header and claims carry the grant: ```json +// protected header +{ "alg": "EdDSA", "typ": "fora-delegation+jwt" } +// claims { "iss": "marketdata.example.com", "sub": "enterprise-sub-HF-001", + "aud": ["exchange.marketdata-data.com"], + "jti": "md-hf-001-2026", "scope": "quote:* earnings:* news:read subscription:enterprise-sub-HF-001", "exp": 1798761599, - "cnf": { "jkt": "" } + "cnf": { "jkt": "SfcyfTBzTRhRo-e1PeD-iL-I_X-QDlgAJgvUy6N7oCE" } } ``` -The Exchange verifies the JWT chain: -1. **Signature check** — the authority JWT is signed by `marketdata.example.com`'s Ed25519 key (fetched from the WBA directory at `marketdata.example.com/.well-known/http-message-signatures-directory`). This proves MarketData actually issued this delegation. -2. **Scope matching** — the subscription term's scopes (`subscription:enterprise-sub-HF-001`) are covered by the scopes the request states, and by the scopes the JWT grants, which this Exchange also requires before it presents a subscription term ([Scope-Based Access](/protocol/authentication/#scope-based-access)). -3. **Expiry check** — `exp` is in the future. -4. **Holder binding** — the key that signed the request (RFC 9421) MUST hash to the final `cnf.jkt`, so a stolen token is useless without the matching private key. -5. **No network call to MarketData** — delegation JWTs are offline-verifiable. MarketData does not need to be online or queried. This is the key property that makes delegation scale. +The Exchange verifies the chain as [Verification](/protocol/authentication/#verification) describes. What that means for this request: +1. **Trust is configured** — the root token verifies with the Ed25519 key the Exchange has configured for the issuer `marketdata.example.com` on the subscription record. Nothing is fetched, and a key not on the record verifies nothing. This proves MarketData actually issued this delegation. +2. **Holder binding** — the key that signed this request (RFC 9421) hashes to the token's `cnf.jkt`, so a stolen token is useless without the matching private key. +3. **Scope matching** — the subscription term's scopes (`subscription:enterprise-sub-HF-001`) are covered by the effective scopes the verifier returns. For this gated term the Exchange reads scopes from the verified delegation only and ignores the scopes the request states ([Scope-Based Access](/protocol/authentication/#scope-based-access)). +4. **Effective expiry** — the verifier returns the token's `exp` as the effective expiry, and the subscription offer expires no later than it. +5. **No network call to MarketData** — delegation tokens are offline-verifiable. MarketData does not need to be online or queried. This is the key property that makes delegation scale. -**Narrowing the grant**: If the hedge fund wanted to restrict its agent further (e.g., limit to NVDA only, cap spend at $100), it issues a child delegation JWT (narrowing) — signed by the principal key named in the authority's `cnf` and bound to the agent's key — without going back to MarketData: +**Narrowing the grant**: If the hedge fund wanted to restrict its agent further (e.g., limit to NVDA earnings only, for a shorter time), it issues a child token — signed by the principal key named in the root's `cnf`, bound to the agent's key, carrying the complete root in `fora_parent` — without going back to MarketData: ```json -// child delegation JWT — signed by the principal key, bound to the agent +// child token — protected header carries the principal's public key +{ "alg": "Ed25519", "typ": "fora-delegation+jwt", + "jwk": { "kty": "OKP", "crv": "Ed25519", "x": "" } } +// claims — iss is the thumbprint of that key, which is also the root's cnf.jkt { - "iss": "hedgefund.com", + "iss": "", + "aud": ["exchange.marketdata-data.com"], "scope": "earnings:NVDA", - "fora_max_spend_cents": 10000, - "exp": 1798761599, - "cnf": { "jkt": "" } + "exp": 1790000000, + "cnf": { "jkt": "" }, + "fora_parent": "" } ``` diff --git a/website/src/content/docs/reference/changelog.mdx b/website/src/content/docs/reference/changelog.mdx index 273cee54..584280db 100644 --- a/website/src/content/docs/reference/changelog.mdx +++ b/website/src/content/docs/reference/changelog.mdx @@ -10,8 +10,49 @@ and protocol history, see [`proto/CHANGELOG.md`](https://github.com/FORA-Protoco ### Breaking against v1.1.0 +**Protocol** + +- **Delegation profile.** The holder-bound JWT delegation chain is now a fixed profile + on the authentication page, and the Exchange verifies it as written. The chain is + one compact JWS: each child carries its complete signed parent in the `fora_parent` + claim and its own Ed25519 public key in the protected `jwk` header, at most four + tokens deep. Every token requires the protected header `typ` `fora-delegation+jwt` + and `alg` `EdDSA` or `Ed25519` with an Ed25519 key, and the claims `iss`, `aud`, + `cnf.jkt` and a finite `exp`; `nbf` is honoured, `jti` is recommended, and a claim + the profile does not list is refused, with vendor metadata inside the signed + `fora_advisory` object only. `aud` follows the Request recipient rule. The root's + `iss` is the identity configured with the trusted key, on the subscription record + that owns the gated term; a child's `iss` is the thumbprint of its own signing key. + No key is discovered through OIDC, JWKS or the token. The clock leeway is 60 seconds + and never extends the effective expiry, the chain's earliest `exp`, which bounds a + gated offer's expiry. `Delegation.issuer` and `Delegation.principal_domain` mirror + the root's `iss` and establish nothing. For a term gated behind a verified + delegation the Exchange reads scopes from the chain only and ignores + `Requester.scopes`. Revocation lookup through `Delegation.revocation_uri` is not part + of the profile: withdrawing a configured key is how a chain is invalidated early. The + tokens the documentation shows are real and verified by a conformance test. + +**Wire** + +- **Delegation token size.** `Delegation.token` is at most 32768 bytes, declared as + `bytes.max_len` on the field. The limit holds for the complete chain, every embedded + parent included, and is checked at wire validation before the token is parsed, so a + longer token is an invalid request rather than a per-item denial, whether or not the + bytes are a valid JWT. The generated Pydantic and Zod models, the published JSON + Schemas and the precompiled strict validators carry the same limit as an exact + base64 pattern, because a plain `maxLength` on the base64 string would admit a token + one byte over the limit. The validation corpus carries a 32768-byte token that is + accepted and a 32769-byte token that is refused. + **SDK users** +- **`ScopesSubset` is grant containment.** `helpers.ScopesSubset`, `scopes_subset` and + `scopesSubset` report whether every scope in the first list is covered by some scope + in the second under the protocol's segment-wise rule, not whether it is an exact + member. A child `publisher:news:read` under a parent `publisher:*:read` now passes, + as the proto always said it should, and a child `publisher:news:*` under that parent + still fails. A caller that relied on exact membership sees wildcard grants accepted. + The `subset` section of `scopes-vectors.json` carries the new cases. - **Go client errors.** `connect.CallError` and `resolvers.FetchError` gain a `Header` field, an `http.Header`, so neither struct compares with `==` any more. An RPC `CallError` now carries the `Status` of the peer's error answer, which v1.1.0 left at 0, @@ -19,6 +60,40 @@ and protocol history, see [`proto/CHANGELOG.md`](https://github.com/FORA-Protoco ### Changes since v1.1.0 +**Scope matching and delegation verification in the SDKs.** + +- **Scope matching helpers.** `helpers.ScopeCovers` and `helpers.ScopesCover` (Go), + `scope_covers` and `scopes_cover` (Python), `scopeCovers` and `scopesCover` + (TypeScript) implement the protocol's segment-wise scope rule: each granted segment + equals the required one or is `*`, a terminal `*` covers the remaining segments and + needs at least one, no implicit prefix match, a grant with more segments than the + requirement never covers it, and a `*` in the requirement is a literal segment. All + three are pinned to `sdk/go/helpers/testdata/scope-match-vectors.json`, whose + verdicts are written from the rule, not emitted by an implementation. +- **Delegation chain verifier (Go).** `helpers.VerifyDelegation` verifies a chain + against the profile: the size and depth limits, the protected header, the chain walk + through `fora_parent`, the root under caller-configured `TrustedIssuer` keys, each + child under its `jwk` and the intermediate issuer rule, the claims table, the clock + leeway, attenuation of scopes, audiences and expiry, the audience and the holder + binding. It takes a `DelegationInput` and returns a `VerifiedDelegation` (root + issuer, holder thumbprint, effective scopes, effective expiry, depth) or a + `DelegationError` naming one `DelegationFailure`, which unwraps to + `ErrDelegationInvalid`. It verifies the Ed25519 signature with the standard library + under either `alg` name, because the pinned go-jose accepts only `EdDSA`. It meters + nothing, so a chain carrying `fora_max_spend_cents`, `fora_max_accesses` or + `fora_quota_period` is refused. The constants `MaxDelegationTokenBytes`, + `MaxDelegationChainDepth`, `DelegationClockLeeway`, `DelegationTokenType` and + `MaxDelegationNumericDate` are the profile's values; the last bounds `exp` and `nbf` + to `9999-12-31T23:59:59Z` so the returned expiry always fits a `Timestamp`. The + verifier accepts only unpadded base64url segments and exactly one JSON object as + payload, and refuses a `b64` header parameter. The TypeScript and Python verifiers are deferred and recorded + as a decision in the parity matrix. +- **Shared delegation chain cases.** `sdk/go/helpers/testdata/delegation-chains.json` + carries signed chains from fixed test keys and a fixed clock with outcomes declared + from the profile, one case per rule, plus the chains the documentation embeds. The + Go tests re-verify every case; an Exchange's tests and the deferred ports replay the + same file. + **Three more SDK capabilities for conformance and e2e harnesses (no wire change).** A harness can now read the header fields a refusal arrived with, sign an acceptance that names an empty requester to prove that a receiver refuses it, and, in Python, have the diff --git a/website/src/content/docs/reference/proto-fora.mdx b/website/src/content/docs/reference/proto-fora.mdx index d6afdba2..40e8a5de 100644 --- a/website/src/content/docs/reference/proto-fora.mdx +++ b/website/src/content/docs/reference/proto-fora.mdx @@ -90,15 +90,15 @@ Identity and entitlements only — who is asking and what they're entitled to. W ### Delegation -Scoped, time-limited, spend-capped credential. The credential itself is opaque bytes carried in `token`, and `token_format` selects how to interpret them: `"jwt"` (the default and sole format; the field stays open for a future one). The format is never encoded as a prefix inside the token bytes. The token is a **holder-bound JWT**: a chain of `cnf`-linked JWTs (RFC 7800 `cnf.jkt` = RFC 7638 thumbprint) where each child is signed by the key its parent named and narrows scope, and the holder proves possession by signing the request (RFC 9421). +Scoped, time-limited credential. The credential is carried in `token`, at most 32768 bytes, and `token_format` selects how to interpret it: `"jwt"` (the default and sole format; the field stays open for a future one). The format is never encoded as a prefix inside the token bytes. The token is a **holder-bound JWT chain** nested in one compact JWS: each child carries its complete signed parent in the `fora_parent` claim and its own Ed25519 public key in the protected `jwk` header, that key hashes to the parent's `cnf.jkt` (RFC 7800 `cnf.jkt` = RFC 7638 thumbprint), each child narrows its parent, and the holder proves possession of the key the last `cnf.jkt` names by signing the request (RFC 9421). The root is verified with a key the Exchange has configured for its `iss`; no key is ever fetched. The complete profile, with the required protected header (`typ: fora-delegation+jwt`, `alg: EdDSA` or `Ed25519`), the required claims (`iss`, `aud`, `cnf.jkt`, `exp`), the clock leeway and the limits, is the [Delegation](/protocol/authentication/#delegation) section of the authentication page. ::proto-message{name=Delegation} -**Delegation claims.** The claim vocabulary carried inside the token is **not re-listed here** — the authoritative registry is the [Delegation-Claims Profile](/protocol/authentication/#delegation-claims-profile) table in `authentication.mdx`. The one claim worth restating: every delegation **MUST** carry a **holder binding** (the `cnf` claim, `cnf.jkt` = the RFC 7638 thumbprint of the holder key) — the request-signing key MUST hash to `cnf.jkt`, or the token is rejected. Without it the token is bearer-usable. On a purchase a Broker re-packaged (`BrokerService.ExecuteTransaction`), the request reaching the Exchange is signed by the Broker, so the Exchange checks `cnf.jkt` against the key each item's `AgentAcceptance` verifies under instead of the request signer. Everything else (scope, spend cap, expiry, etc.) is optional and defined in that single source of truth. +**Delegation claims.** The claim vocabulary carried inside the token is **not re-listed here** — the authoritative registry is the [Delegation-Claims Profile](/protocol/authentication/#delegation-claims-profile) table in `authentication.mdx`. Two points are worth restating. Every token **MUST** carry `iss`, `aud`, a finite `exp` and the **holder binding** (`cnf.jkt` = the RFC 7638 thumbprint of the holder key): the key the request proves possession of MUST hash to the last token's `cnf.jkt`, or the chain is rejected; without it the token would be bearer-usable. On a purchase a Broker re-packaged (`BrokerService.ExecuteTransaction`), the request reaching the Exchange is signed by the Broker, so the Exchange checks `cnf.jkt` against the key each item's `AgentAcceptance` verifies under instead of the request signer. And a claim the profile does not list is rejected, never ignored. The plaintext fields of this message mirror the chain (`issuer` and `principal_domain` mirror the root's `iss`) and establish nothing. -**Narrowing example**: An agent can further restrict (but never widen) a delegation by issuing a child JWT: -- Original: `scope: credit:*, max_spend_cents: 100000` -- Attenuated: `scope: credit:read, max_spend_cents: 20000, uris: duns:123*` +**Narrowing example**: An agent can further restrict (but never widen) a delegation by issuing a child token: +- Root: `scope: credit:* dist:*`, `exp` in two hours +- Child: `scope: credit:read dist:US:*`, `exp` in one hour, signed by the key the root's `cnf.jkt` names, with the root inside `fora_parent` ### Multi-hop forwarding (no message — HTTP layer) diff --git a/website/src/content/docs/security/threat-model.mdx b/website/src/content/docs/security/threat-model.mdx index e7867ef9..77e2bbfa 100644 --- a/website/src/content/docs/security/threat-model.mdx +++ b/website/src/content/docs/security/threat-model.mdx @@ -244,17 +244,17 @@ Line 3: Legal/contractual enforcement ### T-DEL-1: Delegation token theft **Attack**: Attacker steals a delegation token (e.g., via log exfiltration, memory dump, or network interception) and attempts to use it to access resources within the token's scope. -**Countermeasure (protocol)**: **Holder binding neutralizes theft.** A delegation token is bound to the holder's key (via `cnf.jkt` on the JWT), and verification requires the key the request proves possession of — the key that signs the HTTP request ([RFC 9421](/protocol/authentication/#request-signatures-rfc-9421)) on a direct call, the key the agent's `AgentAcceptance` verifies under on a purchase a Broker relays — to equal the bound key (see [Verification](/protocol/authentication/#verification) step 3). A thief who steals the token bytes but not the private signing key cannot produce a valid request — the token is **not bearer-usable**, so theft alone grants nothing. Spend caps (`max_spend_cents`), access caps (`max_accesses`), short TTLs, and optional `revocation_uri` checking are **defense-in-depth** that bound the blast radius in the residual case where the holder's signing key is *also* compromised. This is why T-DEL-1 is listed as structurally preventable rather than merely mitigated. +**Countermeasure (protocol)**: **Holder binding neutralizes theft.** A delegation token is bound to the holder's key (via `cnf.jkt` on the JWT), and verification requires the key the request proves possession of — the key that signs the HTTP request ([RFC 9421](/protocol/authentication/#request-signatures-rfc-9421)) on a direct call, the key the agent's `AgentAcceptance` verifies under on a purchase a Broker relays — to equal the bound key (see [Verification](/protocol/authentication/#verification)). A thief who steals the token bytes but not the private signing key cannot produce a valid request — the token is **not bearer-usable**, so theft alone grants nothing. Short expiry, the `aud` claim that names the one Exchange a token is for, and withdrawal of the issuer's configured key are **defense-in-depth** that bound the blast radius in the residual case where the holder's signing key is *also* compromised. This is why T-DEL-1 is listed as structurally preventable rather than merely mitigated. ### T-DEL-2: Delegation scope escalation **Attack**: Attacker attempts to widen permissions by manipulating the delegation chain (e.g., editing a child JWT to escalate from `ai-search` to `ai-train`, or splicing in a forged link). -**Countermeasure (protocol)**: **The delegation chain narrows only, and is cryptographically linked.** Each delegation JWT is Ed25519-signed, and a child's `scope` MUST be covered by its parent's (verified) — it can narrow, never widen. The **chain-linkage invariant** requires each JWT to be signed by the key the parent named in `cnf`; editing or splicing a link breaks signature verification against the upstream `cnf`. The Exchange verifies the full chain (and the key the request proves possession of against the final `cnf.jkt`) before honouring the delegation. +**Countermeasure (protocol)**: **The delegation chain narrows only, and is cryptographically linked.** Each delegation JWT is Ed25519-signed, and a child's `scope` MUST be covered by its parent's (verified) — it can narrow, never widen. The **chain-linkage invariant** requires each token to be signed by the key the parent named in `cnf`, and each child carries its complete parent inside its own signed `fora_parent` claim, so editing a link breaks that link's signature and splicing a different parent breaks the `jwk`-to-`cnf.jkt` match. A claim the profile does not list is refused rather than ignored, so an attacker cannot smuggle a restriction past a verifier that does not understand it. The Exchange verifies the full chain, root first with a configured key, and the key the request proves possession of against the final `cnf.jkt`, before honouring the delegation. ### T-DEL-3: Revocation bypass **Attack**: Principal revokes a delegation but cached or stored tokens continue to work until their `expires_at` timestamp. -**Countermeasure (protocol)**: Short `expires_at` (2-4 hours for standard access) limits the window of continued access. For high-value transactions, Exchange checks `revocation_uri` in real time before fulfillment. **Trade-off**: revocation checking adds latency (~50ms) so is only used for transactions above a configurable value threshold. +**Countermeasure (protocol)**: Short `exp` (2-4 hours for standard access) limits the window of continued access; the verifier allows 60 seconds of clock skew and no more. Trust is configured per subscription record, so removing the issuer's key from that record refuses every chain it signed at the next request. Per-token revocation through `revocation_uri` is not part of this profile. ### T-DEL-4: Signer impersonation and forged forwarding