lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit 2cf1ed57c894376ca99bf3af4d3416feb8bcf093
parent f600dbf5bd5df8c2faa2b8e05ff7630bd043f5e1
Author: triesap <tyson@radroots.org>
Date:   Wed, 22 Jul 2026 03:54:58 +0000

event-codec: enforce Phase 1 publication allowlist

- admit only the seven sealed publication leaves through typed and canonical adapters
- reproject kind profiles with strict post, calendar, and FoodAvailability rules
- execute 39 allowlist vectors while freezing registry-v7 and predecessor bytes
- govern the successor manifest, public documentation, and release metadata

Diffstat:
MCHANGELOG.md | 15+++++++++++++++
Acontracts/conformance/vectors/publication/phase1_allowlist.v1.json | 549+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcontracts/operations.toml | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcontracts/releases/1.0.0-alpha.1.toml | 11+++++++++++
Mcrates/event_codec/README | 18++++++++++++++++++
Acrates/event_codec/contracts/phase1_publication_allowlist_v1.descriptor.json | 24++++++++++++++++++++++++
Acrates/event_codec/contracts/phase1_publication_allowlist_v1.manifest.json | 2307+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/event_codec/contracts/phase1_publication_allowlist_v1.manifest.schema.json | 352+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/event_codec/contracts/phase1_publication_allowlist_v1.manifest.sha256 | 1+
Mcrates/event_codec/src/wire/publication.rs | 2++
Acrates/event_codec/src/wire/publication/allowlist.rs | 544+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/event_codec/tests/fixtures/phase1_publication_allowlist.v1.json | 549+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/event_codec/tests/publication_allowlist.rs | 224+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtools/xtask/src/contract.rs | 17+++++++++++++++--
Atools/xtask/src/contract/phase1_publication_allowlist.rs | 1986+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtools/xtask/src/contract/phase1_publication_artifact.rs | 150+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mtools/xtask/src/main.rs | 19+++++++++++++++++++
17 files changed, 6826 insertions(+), 8 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -184,6 +184,21 @@ publish policy both pass for the same source revision. encodings, cross-profile promotion, stale identifiers, media drift, and digest tampering without claiming restored byte verification, upload completion, signature authenticity, or signer authority. +<!-- release-change: phase1-publication-allowlist --> +- The event-codec publication surface now exposes an additive allowlist over + the sealed Phase 1 artifact. Its opaque output admits exactly supporting + Profile plus root Update, PhotoUpdate, Ask, typed date/time Event, and focused + FoodAvailability leaves. Registry-v7 reprojection enforces + Ask-before-PhotoUpdate-before-Update precedence and marker-partitions + kind-`30402` before profile validation. A canonical-JSON adapter composes + strict artifact reload with the same allowlist for durable consumers. Raw + and generic events, including raw date/time events, replies, comments, + deletion requests, calendar collections and RSVPs, BUD-11 authorizations, + ephemeral events, trade, commerce, group, and operations families cannot + produce its sealed input; deferred route/delivery product surfaces are + non-events and likewise fail closed. Event-contract registry v7 remains + byte-identical, and the new gate grants no signing, upload, retrieval, relay, + or entitlement authority. - Bare-envelope replica ingestion is quarantined behind the explicit, non-default `legacy-ingest` feature. Default replica APIs expose emit and sync surfaces only; a future product ingest boundary must consume a store-produced diff --git a/contracts/conformance/vectors/publication/phase1_allowlist.v1.json b/contracts/conformance/vectors/publication/phase1_allowlist.v1.json @@ -0,0 +1,549 @@ +{ + "contract_version": "1.0.0", + "suite": "phase1_publication_allowlist", + "vectors": [ + { + "expected": { + "decision": "allow", + "event_kind": 0, + "leaf": "profile" + }, + "id": "sealed_profile_is_allowed", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"profile\",\"authored_operation_id\":\"profile.build_authored_draft\",\"event_contract_id\":\"radroots.profile.metadata.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":0,\"tags\":[],\"content\":\"{\\\"name\\\":\\\"victoria-farm\\\",\\\"display_name\\\":\\\"Victoria Farm\\\",\\\"about\\\":\\\"Seasonal produce from the Saanich Peninsula\\\",\\\"picture\\\":\\\"https://media.example/512f6a371c77694502e7d08f0b1f1080c7103ca90925bfe2fa23106aac11003a.png\\\",\\\"banner\\\":\\\"https://media.example/fbf3819415a76ea0d3ba71817578bb89c4903aa958e38f76f86578dfa8a35145.webp\\\",\\\"nip05\\\":\\\"farm@example.com\\\",\\\"bot\\\":false}\"},\"expected_event_id\":\"a7d081b9c142e3a68245bafe1921b0e0dd88f886406e7165a9729f6c57026289\",\"media_references\":[{\"url\":\"https://media.example/512f6a371c77694502e7d08f0b1f1080c7103ca90925bfe2fa23106aac11003a.png\",\"sha256\":\"512f6a371c77694502e7d08f0b1f1080c7103ca90925bfe2fa23106aac11003a\",\"size\":15,\"media_type\":\"image/png\"},{\"url\":\"https://media.example/fbf3819415a76ea0d3ba71817578bb89c4903aa958e38f76f86578dfa8a35145.webp\",\"sha256\":\"fbf3819415a76ea0d3ba71817578bb89c4903aa958e38f76f86578dfa8a35145\",\"size\":14,\"media_type\":\"image/webp\"}],\"artifact_digest\":\"e296ed63312d58b4e3ee160d4ca3457eb922920d3116329ca498f08e402215b0\"}", + "surface": "sealed_artifact_json" + }, + "kind": "publication_allowlist.allow_artifact.valid" + }, + { + "expected": { + "decision": "allow", + "event_kind": 1, + "leaf": "update" + }, + "id": "sealed_update_is_allowed", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"update\",\"authored_operation_id\":\"social.update.build_authored_draft\",\"event_contract_id\":\"radroots.social.update.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":1,\"tags\":[],\"content\":\"Carrots harvested today\"},\"expected_event_id\":\"7f2d9dbbd6d2f3db1e83338ea2e669b2458e62ded159de15a3fa98dcf9f164e3\",\"media_references\":[],\"artifact_digest\":\"9ad318496bd4a710fbc7f3e0f6d5a01de808d352db91ca56a12e710904784cc5\"}", + "surface": "sealed_artifact_json" + }, + "kind": "publication_allowlist.allow_artifact.valid" + }, + { + "expected": { + "decision": "allow", + "event_kind": 1, + "leaf": "photo_update" + }, + "id": "sealed_photo_update_is_allowed", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"photo_update\",\"authored_operation_id\":\"social.photo_update.build_authored_draft\",\"event_contract_id\":\"radroots.social.photo_update.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":1,\"tags\":[[\"imeta\",\"url https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"x 51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"m image/webp\",\"dim 1200x900\",\"size 13\",\"alt Fresh strawberries\",\"fallback https://backup.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\"]],\"content\":\"Strawberries at the farm stand https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\"},\"expected_event_id\":\"e592229776cd2a0d0e25a701f4629c0a5dc6e08481a235623d50da6ee8e8f438\",\"media_references\":[{\"url\":\"https://backup.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"sha256\":\"51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"size\":13,\"media_type\":\"image/webp\"},{\"url\":\"https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"sha256\":\"51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"size\":13,\"media_type\":\"image/webp\"}],\"artifact_digest\":\"9ed6b7a6fcec3b07bd0c9179fd5b6ecf74517390051e3c2bd1659be62a26c197\"}", + "surface": "sealed_artifact_json" + }, + "kind": "publication_allowlist.allow_artifact.valid" + }, + { + "expected": { + "decision": "allow", + "event_kind": 1, + "leaf": "ask" + }, + "id": "sealed_ask_is_allowed", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"ask\",\"authored_operation_id\":\"social.ask.build_authored_draft\",\"event_contract_id\":\"radroots.social.ask.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":1,\"tags\":[[\"t\",\"radroots-ask\"],[\"imeta\",\"url https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"x 51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"m image/webp\",\"dim 1200x900\",\"size 13\",\"alt Fresh strawberries\",\"fallback https://backup.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\"]],\"content\":\"When will strawberries be ready? https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\"},\"expected_event_id\":\"bd45fbbb5bfb8a5aa32df81d240e5f3bd56b0f8cde3a20b3f7d455801373fe48\",\"media_references\":[{\"url\":\"https://backup.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"sha256\":\"51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"size\":13,\"media_type\":\"image/webp\"},{\"url\":\"https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"sha256\":\"51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"size\":13,\"media_type\":\"image/webp\"}],\"artifact_digest\":\"41eddea199bba97adebef4ce433ebfa05d97978ce8464ed3d52762ab717c2a3c\"}", + "surface": "sealed_artifact_json" + }, + "kind": "publication_allowlist.allow_artifact.valid" + }, + { + "expected": { + "decision": "allow", + "event_kind": 31922, + "leaf": "event_date" + }, + "id": "sealed_date_event_is_allowed", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"event_date\",\"authored_operation_id\":\"social.calendar_date_event.build_authored_draft\",\"event_contract_id\":\"radroots.calendar.date_event.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":31922,\"tags\":[[\"d\",\"farmers-market-2026\"],[\"title\",\"Moss Street Farmers Market\"],[\"start\",\"2026-07-25\"],[\"end\",\"2026-07-26\"],[\"location\",\"Victoria, BC\"],[\"image\",\"https://events.example/0a422cbf828d421341c40c678f4cfbd6451841760db126e5f5ac3d2e06fd80b8.jpeg\"]],\"content\":\"Saturday market in Victoria\"},\"expected_event_id\":\"bea82662b6acf2f1272c8335a24bb0957e35d22c2b29d3e30c094ad7398dc7c7\",\"media_references\":[{\"url\":\"https://events.example/0a422cbf828d421341c40c678f4cfbd6451841760db126e5f5ac3d2e06fd80b8.jpeg\",\"sha256\":\"0a422cbf828d421341c40c678f4cfbd6451841760db126e5f5ac3d2e06fd80b8\",\"size\":10,\"media_type\":\"image/jpeg\"}],\"artifact_digest\":\"0ec5162ab80988999aac62329d4cfc9c7efce6a82bbda539a723347d73596eb3\"}", + "surface": "sealed_artifact_json" + }, + "kind": "publication_allowlist.allow_artifact.valid" + }, + { + "expected": { + "decision": "allow", + "event_kind": 31923, + "leaf": "event_time" + }, + "id": "sealed_time_event_is_allowed", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"event_time\",\"authored_operation_id\":\"social.calendar_time_event.build_authored_draft\",\"event_contract_id\":\"radroots.calendar.time_event.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":31923,\"tags\":[[\"d\",\"farm-tour-2026\"],[\"title\",\"Saanich Farm Tour\"],[\"start\",\"1785003600\"],[\"end\",\"1785007200\"],[\"D\",\"20659\"],[\"start_tzid\",\"America/Vancouver\"],[\"image\",\"https://events.example/0a422cbf828d421341c40c678f4cfbd6451841760db126e5f5ac3d2e06fd80b8.jpeg\"]],\"content\":\"A one-hour farm tour\"},\"expected_event_id\":\"2088ed11cd8c1495a82e6f5198ed2ee98e0cbc599f22d7f3380892155dc55894\",\"media_references\":[{\"url\":\"https://events.example/0a422cbf828d421341c40c678f4cfbd6451841760db126e5f5ac3d2e06fd80b8.jpeg\",\"sha256\":\"0a422cbf828d421341c40c678f4cfbd6451841760db126e5f5ac3d2e06fd80b8\",\"size\":10,\"media_type\":\"image/jpeg\"}],\"artifact_digest\":\"56bfb4bed87559a58922343905a73c0c96dd0a57f94ff0955caecf0589198638\"}", + "surface": "sealed_artifact_json" + }, + "kind": "publication_allowlist.allow_artifact.valid" + }, + { + "expected": { + "decision": "allow", + "event_kind": 30402, + "leaf": "food_availability" + }, + "id": "sealed_food_availability_is_allowed", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"food_availability\",\"authored_operation_id\":\"food_availability.build_authored_draft\",\"event_contract_id\":\"radroots.food.availability.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":30402,\"tags\":[[\"d\",\"nantes-carrots\"],[\"title\",\"Nantes Carrots\"],[\"summary\",\"Fresh bunches\"],[\"published_at\",\"1784347140\"],[\"location\",\"Central Saanich, BC\"],[\"price\",\"3\",\"CAD\"],[\"radroots:price_unit\",\"lb\"],[\"radroots:quantity\",\"24\",\"lb\"],[\"status\",\"active\"],[\"image\",\"https://food.example/8a66441b557b29193781023160b0216c5518e9b84749f29ecf118a728f451a5e.png\",\"1200x800\"]],\"content\":\"Fresh Nantes carrots available this week.\"},\"expected_event_id\":\"76b60f1b178c0dbdc14a87de36151ea6b04a866ce50e34a0ccae2f406f514e81\",\"media_references\":[{\"url\":\"https://food.example/8a66441b557b29193781023160b0216c5518e9b84749f29ecf118a728f451a5e.png\",\"sha256\":\"8a66441b557b29193781023160b0216c5518e9b84749f29ecf118a728f451a5e\",\"size\":14,\"media_type\":\"image/png\"}],\"artifact_digest\":\"f0989cb993194af4a2f907ad8337e50378d2757c536a09afb4055a681043a7d8\"}", + "surface": "sealed_artifact_json" + }, + "kind": "publication_allowlist.allow_artifact.valid" + }, + { + "expected": { + "decision": "allow", + "event_kind": 0, + "leaf": "profile" + }, + "id": "canonical_profile_is_allowed", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"profile\",\"authored_operation_id\":\"profile.build_authored_draft\",\"event_contract_id\":\"radroots.profile.metadata.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":0,\"tags\":[],\"content\":\"{\\\"name\\\":\\\"victoria-farm\\\",\\\"display_name\\\":\\\"Victoria Farm\\\",\\\"about\\\":\\\"Seasonal produce from the Saanich Peninsula\\\",\\\"picture\\\":\\\"https://media.example/512f6a371c77694502e7d08f0b1f1080c7103ca90925bfe2fa23106aac11003a.png\\\",\\\"banner\\\":\\\"https://media.example/fbf3819415a76ea0d3ba71817578bb89c4903aa958e38f76f86578dfa8a35145.webp\\\",\\\"nip05\\\":\\\"farm@example.com\\\",\\\"bot\\\":false}\"},\"expected_event_id\":\"a7d081b9c142e3a68245bafe1921b0e0dd88f886406e7165a9729f6c57026289\",\"media_references\":[{\"url\":\"https://media.example/512f6a371c77694502e7d08f0b1f1080c7103ca90925bfe2fa23106aac11003a.png\",\"sha256\":\"512f6a371c77694502e7d08f0b1f1080c7103ca90925bfe2fa23106aac11003a\",\"size\":15,\"media_type\":\"image/png\"},{\"url\":\"https://media.example/fbf3819415a76ea0d3ba71817578bb89c4903aa958e38f76f86578dfa8a35145.webp\",\"sha256\":\"fbf3819415a76ea0d3ba71817578bb89c4903aa958e38f76f86578dfa8a35145\",\"size\":14,\"media_type\":\"image/webp\"}],\"artifact_digest\":\"e296ed63312d58b4e3ee160d4ca3457eb922920d3116329ca498f08e402215b0\"}", + "surface": "canonical_artifact_json" + }, + "kind": "publication_allowlist.allow_canonical_json.valid" + }, + { + "expected": { + "decision": "allow", + "event_kind": 1, + "leaf": "update" + }, + "id": "canonical_update_is_allowed", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"update\",\"authored_operation_id\":\"social.update.build_authored_draft\",\"event_contract_id\":\"radroots.social.update.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":1,\"tags\":[],\"content\":\"Carrots harvested today\"},\"expected_event_id\":\"7f2d9dbbd6d2f3db1e83338ea2e669b2458e62ded159de15a3fa98dcf9f164e3\",\"media_references\":[],\"artifact_digest\":\"9ad318496bd4a710fbc7f3e0f6d5a01de808d352db91ca56a12e710904784cc5\"}", + "surface": "canonical_artifact_json" + }, + "kind": "publication_allowlist.allow_canonical_json.valid" + }, + { + "expected": { + "decision": "allow", + "event_kind": 1, + "leaf": "photo_update" + }, + "id": "canonical_photo_update_is_allowed", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"photo_update\",\"authored_operation_id\":\"social.photo_update.build_authored_draft\",\"event_contract_id\":\"radroots.social.photo_update.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":1,\"tags\":[[\"imeta\",\"url https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"x 51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"m image/webp\",\"dim 1200x900\",\"size 13\",\"alt Fresh strawberries\",\"fallback https://backup.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\"]],\"content\":\"Strawberries at the farm stand https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\"},\"expected_event_id\":\"e592229776cd2a0d0e25a701f4629c0a5dc6e08481a235623d50da6ee8e8f438\",\"media_references\":[{\"url\":\"https://backup.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"sha256\":\"51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"size\":13,\"media_type\":\"image/webp\"},{\"url\":\"https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"sha256\":\"51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"size\":13,\"media_type\":\"image/webp\"}],\"artifact_digest\":\"9ed6b7a6fcec3b07bd0c9179fd5b6ecf74517390051e3c2bd1659be62a26c197\"}", + "surface": "canonical_artifact_json" + }, + "kind": "publication_allowlist.allow_canonical_json.valid" + }, + { + "expected": { + "decision": "allow", + "event_kind": 1, + "leaf": "ask" + }, + "id": "canonical_ask_is_allowed", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"ask\",\"authored_operation_id\":\"social.ask.build_authored_draft\",\"event_contract_id\":\"radroots.social.ask.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":1,\"tags\":[[\"t\",\"radroots-ask\"],[\"imeta\",\"url https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"x 51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"m image/webp\",\"dim 1200x900\",\"size 13\",\"alt Fresh strawberries\",\"fallback https://backup.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\"]],\"content\":\"When will strawberries be ready? https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\"},\"expected_event_id\":\"bd45fbbb5bfb8a5aa32df81d240e5f3bd56b0f8cde3a20b3f7d455801373fe48\",\"media_references\":[{\"url\":\"https://backup.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"sha256\":\"51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"size\":13,\"media_type\":\"image/webp\"},{\"url\":\"https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"sha256\":\"51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"size\":13,\"media_type\":\"image/webp\"}],\"artifact_digest\":\"41eddea199bba97adebef4ce433ebfa05d97978ce8464ed3d52762ab717c2a3c\"}", + "surface": "canonical_artifact_json" + }, + "kind": "publication_allowlist.allow_canonical_json.valid" + }, + { + "expected": { + "decision": "allow", + "event_kind": 31922, + "leaf": "event_date" + }, + "id": "canonical_date_event_is_allowed", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"event_date\",\"authored_operation_id\":\"social.calendar_date_event.build_authored_draft\",\"event_contract_id\":\"radroots.calendar.date_event.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":31922,\"tags\":[[\"d\",\"farmers-market-2026\"],[\"title\",\"Moss Street Farmers Market\"],[\"start\",\"2026-07-25\"],[\"end\",\"2026-07-26\"],[\"location\",\"Victoria, BC\"],[\"image\",\"https://events.example/0a422cbf828d421341c40c678f4cfbd6451841760db126e5f5ac3d2e06fd80b8.jpeg\"]],\"content\":\"Saturday market in Victoria\"},\"expected_event_id\":\"bea82662b6acf2f1272c8335a24bb0957e35d22c2b29d3e30c094ad7398dc7c7\",\"media_references\":[{\"url\":\"https://events.example/0a422cbf828d421341c40c678f4cfbd6451841760db126e5f5ac3d2e06fd80b8.jpeg\",\"sha256\":\"0a422cbf828d421341c40c678f4cfbd6451841760db126e5f5ac3d2e06fd80b8\",\"size\":10,\"media_type\":\"image/jpeg\"}],\"artifact_digest\":\"0ec5162ab80988999aac62329d4cfc9c7efce6a82bbda539a723347d73596eb3\"}", + "surface": "canonical_artifact_json" + }, + "kind": "publication_allowlist.allow_canonical_json.valid" + }, + { + "expected": { + "decision": "allow", + "event_kind": 31923, + "leaf": "event_time" + }, + "id": "canonical_time_event_is_allowed", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"event_time\",\"authored_operation_id\":\"social.calendar_time_event.build_authored_draft\",\"event_contract_id\":\"radroots.calendar.time_event.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":31923,\"tags\":[[\"d\",\"farm-tour-2026\"],[\"title\",\"Saanich Farm Tour\"],[\"start\",\"1785003600\"],[\"end\",\"1785007200\"],[\"D\",\"20659\"],[\"start_tzid\",\"America/Vancouver\"],[\"image\",\"https://events.example/0a422cbf828d421341c40c678f4cfbd6451841760db126e5f5ac3d2e06fd80b8.jpeg\"]],\"content\":\"A one-hour farm tour\"},\"expected_event_id\":\"2088ed11cd8c1495a82e6f5198ed2ee98e0cbc599f22d7f3380892155dc55894\",\"media_references\":[{\"url\":\"https://events.example/0a422cbf828d421341c40c678f4cfbd6451841760db126e5f5ac3d2e06fd80b8.jpeg\",\"sha256\":\"0a422cbf828d421341c40c678f4cfbd6451841760db126e5f5ac3d2e06fd80b8\",\"size\":10,\"media_type\":\"image/jpeg\"}],\"artifact_digest\":\"56bfb4bed87559a58922343905a73c0c96dd0a57f94ff0955caecf0589198638\"}", + "surface": "canonical_artifact_json" + }, + "kind": "publication_allowlist.allow_canonical_json.valid" + }, + { + "expected": { + "decision": "allow", + "event_kind": 30402, + "leaf": "food_availability" + }, + "id": "canonical_food_availability_is_allowed", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"food_availability\",\"authored_operation_id\":\"food_availability.build_authored_draft\",\"event_contract_id\":\"radroots.food.availability.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":30402,\"tags\":[[\"d\",\"nantes-carrots\"],[\"title\",\"Nantes Carrots\"],[\"summary\",\"Fresh bunches\"],[\"published_at\",\"1784347140\"],[\"location\",\"Central Saanich, BC\"],[\"price\",\"3\",\"CAD\"],[\"radroots:price_unit\",\"lb\"],[\"radroots:quantity\",\"24\",\"lb\"],[\"status\",\"active\"],[\"image\",\"https://food.example/8a66441b557b29193781023160b0216c5518e9b84749f29ecf118a728f451a5e.png\",\"1200x800\"]],\"content\":\"Fresh Nantes carrots available this week.\"},\"expected_event_id\":\"76b60f1b178c0dbdc14a87de36151ea6b04a866ce50e34a0ccae2f406f514e81\",\"media_references\":[{\"url\":\"https://food.example/8a66441b557b29193781023160b0216c5518e9b84749f29ecf118a728f451a5e.png\",\"sha256\":\"8a66441b557b29193781023160b0216c5518e9b84749f29ecf118a728f451a5e\",\"size\":14,\"media_type\":\"image/png\"}],\"artifact_digest\":\"f0989cb993194af4a2f907ad8337e50378d2757c536a09afb4055a681043a7d8\"}", + "surface": "canonical_artifact_json" + }, + "kind": "publication_allowlist.allow_canonical_json.valid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_post_profile_invalid" + }, + "id": "update_claim_with_ask_marker_is_rejected", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"update\",\"authored_operation_id\":\"social.update.build_authored_draft\",\"event_contract_id\":\"radroots.social.update.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":1,\"tags\":[[\"t\",\"radroots-ask\"]],\"content\":\"Carrots harvested today\"},\"expected_event_id\":\"bce463fddb9395ba11d4b8500379a39e14afd1472b3cf47e22b6b9f2a438353e\",\"media_references\":[],\"artifact_digest\":\"0000000000000000000000000000000000000000000000000000000000000000\"}", + "surface": "artifact_candidate_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_post_profile_invalid" + }, + "id": "photo_claim_with_ask_marker_is_rejected", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"photo_update\",\"authored_operation_id\":\"social.photo_update.build_authored_draft\",\"event_contract_id\":\"radroots.social.photo_update.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":1,\"tags\":[[\"t\",\"radroots-ask\"],[\"imeta\",\"url https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"x 51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"m image/webp\",\"dim 1200x900\",\"size 13\",\"alt Fresh strawberries\",\"fallback https://backup.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\"]],\"content\":\"Strawberries at the farm stand https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\"},\"expected_event_id\":\"57afde82467e5d72aa07ee8cf1db454945a1615295e4e112d9ffd89e987c10f4\",\"media_references\":[{\"url\":\"https://backup.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"sha256\":\"51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"size\":13,\"media_type\":\"image/webp\"},{\"url\":\"https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"sha256\":\"51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"size\":13,\"media_type\":\"image/webp\"}],\"artifact_digest\":\"0000000000000000000000000000000000000000000000000000000000000000\"}", + "surface": "artifact_candidate_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_post_profile_invalid" + }, + "id": "ask_claim_without_marker_is_rejected", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"ask\",\"authored_operation_id\":\"social.ask.build_authored_draft\",\"event_contract_id\":\"radroots.social.ask.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":1,\"tags\":[[\"imeta\",\"url https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"x 51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"m image/webp\",\"dim 1200x900\",\"size 13\",\"alt Fresh strawberries\",\"fallback https://backup.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\"]],\"content\":\"When will strawberries be ready? https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\"},\"expected_event_id\":\"3f67e9ff31c26764bc1c74ef9cafd570a7a7454b3e3768be8f8c1a6175b3d7db\",\"media_references\":[{\"url\":\"https://backup.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"sha256\":\"51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"size\":13,\"media_type\":\"image/webp\"},{\"url\":\"https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"sha256\":\"51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"size\":13,\"media_type\":\"image/webp\"}],\"artifact_digest\":\"0000000000000000000000000000000000000000000000000000000000000000\"}", + "surface": "artifact_candidate_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_post_profile_invalid" + }, + "id": "update_claim_with_photo_profile_is_rejected", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"update\",\"authored_operation_id\":\"social.update.build_authored_draft\",\"event_contract_id\":\"radroots.social.update.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":1,\"tags\":[[\"imeta\",\"url https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"x 51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"m image/webp\",\"dim 1200x900\",\"size 13\",\"alt Fresh strawberries\",\"fallback https://backup.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\"]],\"content\":\"Strawberries at the farm stand https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\"},\"expected_event_id\":\"e592229776cd2a0d0e25a701f4629c0a5dc6e08481a235623d50da6ee8e8f438\",\"media_references\":[{\"url\":\"https://backup.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"sha256\":\"51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"size\":13,\"media_type\":\"image/webp\"},{\"url\":\"https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"sha256\":\"51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"size\":13,\"media_type\":\"image/webp\"}],\"artifact_digest\":\"0000000000000000000000000000000000000000000000000000000000000000\"}", + "surface": "artifact_candidate_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_calendar_profile_invalid" + }, + "id": "typed_date_event_with_generic_shape_is_rejected", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"event_date\",\"authored_operation_id\":\"social.calendar_date_event.build_authored_draft\",\"event_contract_id\":\"radroots.calendar.date_event.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":31922,\"tags\":[[\"d\",\"generic-calendar\"],[\"title\",\"Generic calendar event\"]],\"content\":\"Saturday market in Victoria\"},\"expected_event_id\":\"10e28f8f61a9bb9affa110bb6cce7cc8e371918aead30fbd31d41792db8f3325\",\"media_references\":[],\"artifact_digest\":\"0000000000000000000000000000000000000000000000000000000000000000\"}", + "family": "generic_calendar", + "surface": "artifact_candidate_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_calendar_profile_invalid" + }, + "id": "typed_time_event_with_generic_shape_is_rejected", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"event_time\",\"authored_operation_id\":\"social.calendar_time_event.build_authored_draft\",\"event_contract_id\":\"radroots.calendar.time_event.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":31923,\"tags\":[[\"d\",\"generic-calendar\"],[\"title\",\"Generic calendar event\"]],\"content\":\"A one-hour farm tour\"},\"expected_event_id\":\"c3da57e519122044ee663cece433d56f0b5d967f8e78a1a1a80260ae7653d6bd\",\"media_references\":[],\"artifact_digest\":\"0000000000000000000000000000000000000000000000000000000000000000\"}", + "family": "generic_calendar", + "surface": "artifact_candidate_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_food_profile_invalid" + }, + "id": "operational_listing_profile_is_rejected", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"food_availability\",\"authored_operation_id\":\"food_availability.build_authored_draft\",\"event_contract_id\":\"radroots.food.availability.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":30402,\"tags\":[[\"d\",\"nantes-carrots\"],[\"title\",\"Nantes Carrots\"],[\"summary\",\"Fresh bunches\"],[\"published_at\",\"1784347140\"],[\"location\",\"Central Saanich, BC\"],[\"price\",\"3\",\"CAD\"],[\"status\",\"active\"],[\"image\",\"https://food.example/8a66441b557b29193781023160b0216c5518e9b84749f29ecf118a728f451a5e.png\",\"1200x800\"],[\"radroots:price\",\"3\",\"CAD\"]],\"content\":\"Fresh Nantes carrots available this week.\"},\"expected_event_id\":\"d752583639bf9edaa5ab25e491be5b565b91af5fb01167377d36fdb319dd61d7\",\"media_references\":[{\"url\":\"https://food.example/8a66441b557b29193781023160b0216c5518e9b84749f29ecf118a728f451a5e.png\",\"sha256\":\"8a66441b557b29193781023160b0216c5518e9b84749f29ecf118a728f451a5e\",\"size\":14,\"media_type\":\"image/png\"}],\"artifact_digest\":\"0000000000000000000000000000000000000000000000000000000000000000\"}", + "family": "operational_listing", + "surface": "artifact_candidate_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_food_profile_invalid" + }, + "id": "generic_nip99_profile_is_rejected", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"food_availability\",\"authored_operation_id\":\"food_availability.build_authored_draft\",\"event_contract_id\":\"radroots.food.availability.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":30402,\"tags\":[[\"d\",\"nantes-carrots\"],[\"title\",\"Nantes Carrots\"],[\"summary\",\"Fresh bunches\"],[\"published_at\",\"1784347140\"],[\"location\",\"Central Saanich, BC\"],[\"price\",\"3\",\"CAD\"],[\"status\",\"active\"],[\"image\",\"https://food.example/8a66441b557b29193781023160b0216c5518e9b84749f29ecf118a728f451a5e.png\",\"1200x800\"]],\"content\":\"Fresh Nantes carrots available this week.\"},\"expected_event_id\":\"6ae040461fe7e6967bb48b2861bc1dbeac075f0331027e2d04b9618e2d752a2d\",\"media_references\":[{\"url\":\"https://food.example/8a66441b557b29193781023160b0216c5518e9b84749f29ecf118a728f451a5e.png\",\"sha256\":\"8a66441b557b29193781023160b0216c5518e9b84749f29ecf118a728f451a5e\",\"size\":14,\"media_type\":\"image/png\"}],\"artifact_digest\":\"0000000000000000000000000000000000000000000000000000000000000000\"}", + "family": "generic_nip99", + "surface": "artifact_candidate_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_food_profile_invalid" + }, + "id": "mixed_food_and_operational_markers_are_rejected", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"food_availability\",\"authored_operation_id\":\"food_availability.build_authored_draft\",\"event_contract_id\":\"radroots.food.availability.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":30402,\"tags\":[[\"d\",\"nantes-carrots\"],[\"title\",\"Nantes Carrots\"],[\"summary\",\"Fresh bunches\"],[\"published_at\",\"1784347140\"],[\"location\",\"Central Saanich, BC\"],[\"price\",\"3\",\"CAD\"],[\"radroots:price_unit\",\"lb\"],[\"radroots:quantity\",\"24\",\"lb\"],[\"status\",\"active\"],[\"image\",\"https://food.example/8a66441b557b29193781023160b0216c5518e9b84749f29ecf118a728f451a5e.png\",\"1200x800\"],[\"radroots:price\",\"3\",\"CAD\"]],\"content\":\"Fresh Nantes carrots available this week.\"},\"expected_event_id\":\"5302117c4c89252c40b57601fe8b115b779bd9256235f449ad86c50ca97c8219\",\"media_references\":[{\"url\":\"https://food.example/8a66441b557b29193781023160b0216c5518e9b84749f29ecf118a728f451a5e.png\",\"sha256\":\"8a66441b557b29193781023160b0216c5518e9b84749f29ecf118a728f451a5e\",\"size\":14,\"media_type\":\"image/png\"}],\"artifact_digest\":\"0000000000000000000000000000000000000000000000000000000000000000\"}", + "family": "mixed_classified_listing", + "surface": "artifact_candidate_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_artifact_invalid_json" + }, + "id": "unsealed_profile_is_rejected", + "input": { + "canonical_json": "{\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"pubkey\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"created_at\":1784347200,\"kind\":0,\"tags\":[],\"content\":\"{\\\"name\\\":\\\"victoria-farm\\\",\\\"display_name\\\":\\\"Victoria Farm\\\",\\\"about\\\":\\\"Seasonal produce from the Saanich Peninsula\\\",\\\"picture\\\":\\\"https://media.example/512f6a371c77694502e7d08f0b1f1080c7103ca90925bfe2fa23106aac11003a.png\\\",\\\"banner\\\":\\\"https://media.example/fbf3819415a76ea0d3ba71817578bb89c4903aa958e38f76f86578dfa8a35145.webp\\\",\\\"nip05\\\":\\\"farm@example.com\\\",\\\"bot\\\":false}\",\"sig\":\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}", + "event_kind": 0, + "family": "unsealed_profile", + "surface": "raw_nip01_event_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_artifact_invalid_json" + }, + "id": "generic_root_note_is_rejected", + "input": { + "canonical_json": "{\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"pubkey\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"created_at\":1784347200,\"kind\":1,\"tags\":[],\"content\":\"Generic root note\",\"sig\":\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}", + "event_kind": 1, + "family": "generic_root_note", + "surface": "raw_nip01_event_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_artifact_invalid_json" + }, + "id": "reply_is_rejected", + "input": { + "canonical_json": "{\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"pubkey\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"created_at\":1784347200,\"kind\":1,\"tags\":[[\"e\",\"1111111111111111111111111111111111111111111111111111111111111111\",\"\",\"reply\"]],\"content\":\"Reply\",\"sig\":\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}", + "event_kind": 1, + "family": "reply", + "surface": "raw_nip01_event_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_artifact_invalid_json" + }, + "id": "comment_is_rejected", + "input": { + "canonical_json": "{\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"pubkey\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"created_at\":1784347200,\"kind\":1111,\"tags\":[[\"E\",\"1111111111111111111111111111111111111111111111111111111111111111\"]],\"content\":\"Comment\",\"sig\":\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}", + "event_kind": 1111, + "family": "comment", + "surface": "raw_nip01_event_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_artifact_invalid_json" + }, + "id": "deletion_request_is_rejected", + "input": { + "canonical_json": "{\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"pubkey\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"created_at\":1784347200,\"kind\":5,\"tags\":[[\"e\",\"1111111111111111111111111111111111111111111111111111111111111111\"]],\"content\":\"Removed\",\"sig\":\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}", + "event_kind": 5, + "family": "deletion_request", + "surface": "raw_nip01_event_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_artifact_invalid_json" + }, + "id": "raw_date_event_is_rejected", + "input": { + "canonical_json": "{\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"pubkey\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"created_at\":1784347200,\"kind\":31922,\"tags\":[[\"d\",\"farmers-market-2026\"],[\"title\",\"Moss Street Farmers Market\"],[\"start\",\"2026-07-25\"],[\"end\",\"2026-07-26\"],[\"location\",\"Victoria, BC\"],[\"image\",\"https://events.example/0a422cbf828d421341c40c678f4cfbd6451841760db126e5f5ac3d2e06fd80b8.jpeg\"]],\"content\":\"Saturday market in Victoria\",\"sig\":\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}", + "event_kind": 31922, + "family": "unsealed_calendar_event", + "surface": "raw_nip01_event_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_artifact_invalid_json" + }, + "id": "raw_time_event_is_rejected", + "input": { + "canonical_json": "{\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"pubkey\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"created_at\":1784347200,\"kind\":31923,\"tags\":[[\"d\",\"farm-tour-2026\"],[\"title\",\"Saanich Farm Tour\"],[\"start\",\"1785003600\"],[\"end\",\"1785007200\"],[\"D\",\"20659\"],[\"start_tzid\",\"America/Vancouver\"],[\"image\",\"https://events.example/0a422cbf828d421341c40c678f4cfbd6451841760db126e5f5ac3d2e06fd80b8.jpeg\"]],\"content\":\"A one-hour farm tour\",\"sig\":\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}", + "event_kind": 31923, + "family": "unsealed_calendar_event", + "surface": "raw_nip01_event_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_artifact_invalid_json" + }, + "id": "calendar_collection_is_rejected", + "input": { + "canonical_json": "{\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"pubkey\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"created_at\":1784347200,\"kind\":31924,\"tags\":[[\"d\",\"victoria-calendar\"]],\"content\":\"\",\"sig\":\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}", + "event_kind": 31924, + "family": "calendar_collection", + "surface": "raw_nip01_event_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_artifact_invalid_json" + }, + "id": "calendar_rsvp_is_rejected", + "input": { + "canonical_json": "{\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"pubkey\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"created_at\":1784347200,\"kind\":31925,\"tags\":[[\"a\",\"31922:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:market\"]],\"content\":\"accepted\",\"sig\":\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}", + "event_kind": 31925, + "family": "calendar_rsvp", + "surface": "raw_nip01_event_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_artifact_invalid_json" + }, + "id": "bud11_authorization_is_rejected", + "input": { + "canonical_json": "{\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"pubkey\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"created_at\":1784347200,\"kind\":24242,\"tags\":[[\"t\",\"upload\"]],\"content\":\"\",\"sig\":\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}", + "event_kind": 24242, + "family": "bud11_authorization", + "surface": "raw_nip01_event_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_artifact_invalid_json" + }, + "id": "ephemeral_event_is_rejected", + "input": { + "canonical_json": "{\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"pubkey\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"created_at\":1784347200,\"kind\":20001,\"tags\":[],\"content\":\"Ephemeral\",\"sig\":\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}", + "event_kind": 20001, + "family": "ephemeral_event", + "surface": "raw_nip01_event_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_artifact_invalid_json" + }, + "id": "trade_event_is_rejected", + "input": { + "canonical_json": "{\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"pubkey\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"created_at\":1784347200,\"kind\":3470,\"tags\":[[\"d\",\"trade-proposal\"]],\"content\":\"\",\"sig\":\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}", + "event_kind": 3470, + "family": "trade", + "surface": "raw_nip01_event_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_artifact_invalid_json" + }, + "id": "order_event_is_rejected", + "input": { + "canonical_json": "{\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"pubkey\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"created_at\":1784347200,\"kind\":3422,\"tags\":[[\"d\",\"order-request\"]],\"content\":\"\",\"sig\":\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}", + "event_kind": 3422, + "family": "commerce_order", + "surface": "raw_nip01_event_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_artifact_invalid_json" + }, + "id": "route_delivery_family_is_rejected", + "input": { + "canonical_json": "{\"product_surface\":\"route_delivery\"}", + "family": "route_delivery", + "surface": "non_event_product_surface" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_artifact_invalid_json" + }, + "id": "group_event_is_rejected", + "input": { + "canonical_json": "{\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"pubkey\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"created_at\":1784347200,\"kind\":9007,\"tags\":[[\"h\",\"victoria-growers\"]],\"content\":\"\",\"sig\":\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}", + "event_kind": 9007, + "family": "group", + "surface": "raw_nip01_event_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_artifact_invalid_json" + }, + "id": "operations_event_is_rejected", + "input": { + "canonical_json": "{\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"pubkey\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"created_at\":1784347200,\"kind\":78,\"tags\":[[\"d\",\"internal-operation\"]],\"content\":\"\",\"sig\":\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}", + "event_kind": 78, + "family": "operations", + "surface": "raw_nip01_event_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + } + ] +} diff --git a/contracts/operations.toml b/contracts/operations.toml @@ -106,6 +106,9 @@ public = [ "RadrootsPhase1PublicationArtifactDigest", "RadrootsPhase1PublicationArtifact", "RadrootsPhase1PublicationArtifactError", + "RadrootsPhase1PublicationLeaf", + "RadrootsPhase1AllowlistedPublicationArtifact", + "RadrootsPhase1PublicationAllowlistError", "RadrootsFarm", "RadrootsOperationalListing", "RadrootsPost", @@ -906,6 +909,69 @@ case_kinds = [ "publication_artifact.from_canonical_json.invalid", ] +[operations.phase1_publication_allowlist_allow_artifact] +domain = "publication" +id = "publication_allowlist.allow_artifact" +stability = "beta" +inputs = ["RadrootsPhase1PublicationArtifact"] +outputs = ["RadrootsPhase1AllowlistedPublicationArtifact"] +error_class = "validation_error" +deterministic = true +signing = "none" +transport = "none" + +[operations.phase1_publication_allowlist_allow_artifact.implementation] +rust_modules = [ + "crates/event_codec/src/food_availability/inbound/registry_v7.rs", + "crates/event_codec/src/post/inbound/registry_v7.rs", + "crates/event_codec/src/wire/publication.rs", + "crates/event_codec/src/wire/publication/allowlist.rs", +] +rust_types = [ + "radroots_event_codec::wire::publication::RadrootsPhase1PublicationArtifact", + "radroots_event_codec::wire::publication::allowlist::RadrootsPhase1AllowlistedPublicationArtifact", + "radroots_event_codec::wire::publication::allowlist::RadrootsPhase1PublicationAllowlistError", + "radroots_event_codec::wire::publication::allowlist::RadrootsPhase1PublicationLeaf", +] + +[operations.phase1_publication_allowlist_allow_artifact.conformance] +vector = "contracts/conformance/vectors/publication/phase1_allowlist.v1.json" +case_kinds = [ + "publication_allowlist.allow_artifact.valid", +] + +[operations.phase1_publication_allowlist_allow_canonical_json] +domain = "publication" +id = "publication_allowlist.allow_canonical_json" +stability = "beta" +inputs = ["Bytes"] +outputs = ["RadrootsPhase1AllowlistedPublicationArtifact"] +error_class = "validation_error" +deterministic = true +signing = "none" +transport = "none" + +[operations.phase1_publication_allowlist_allow_canonical_json.implementation] +rust_modules = [ + "crates/event_codec/src/food_availability/inbound/registry_v7.rs", + "crates/event_codec/src/post/inbound/registry_v7.rs", + "crates/event_codec/src/wire/publication.rs", + "crates/event_codec/src/wire/publication/allowlist.rs", +] +rust_types = [ + "radroots_event_codec::wire::publication::RadrootsPhase1PublicationArtifact", + "radroots_event_codec::wire::publication::allowlist::RadrootsPhase1AllowlistedPublicationArtifact", + "radroots_event_codec::wire::publication::allowlist::RadrootsPhase1PublicationAllowlistError", + "radroots_event_codec::wire::publication::allowlist::RadrootsPhase1PublicationLeaf", +] + +[operations.phase1_publication_allowlist_allow_canonical_json.conformance] +vector = "contracts/conformance/vectors/publication/phase1_allowlist.v1.json" +case_kinds = [ + "publication_allowlist.allow_canonical_json.valid", + "publication_allowlist.allow_canonical_json.invalid", +] + [operations.profile_build_authored_draft] domain = "profile" id = "profile.build_authored_draft" diff --git a/contracts/releases/1.0.0-alpha.1.toml b/contracts/releases/1.0.0-alpha.1.toml @@ -288,6 +288,17 @@ semver_impacts = [ summary = "Add the authored-only Phase 1 publication artifact with seven typed leaves, exact top-level NIP-01 id placement, explicit canonical-byte encode/decode operations, bounded signed-event and artifact wires, complete Blossom URL commitments, and a single-NUL domain-separated digest." [[changes]] +id = "phase1-publication-allowlist" +classification = "feature" +semver_impacts = [ + "add_exported_type", + "add_exported_function", + "add_exported_constant", + "add_conformance_vector", +] +summary = "Add an opaque publication-lane allowlist and canonical-JSON durable-reload adapter over sealed Phase 1 artifacts with one supporting Profile leaf and six root leaves, strict same-kind reprojection, typed-only date/time Event handling, focused FoodAvailability marker partitioning, and fail-closed exclusion of every other generic, ephemeral, or deferred product family." + +[[changes]] id = "event-store-validity-visibility-split" classification = "breaking" semver_impacts = [ diff --git a/crates/event_codec/README b/crates/event_codec/README @@ -7,6 +7,24 @@ are `profile`, `update`, `photo_update`, `ask`, `event_date`, `event_time`, and `food_availability`; Event remains one semantic role with typed date and time subvariants. +`wire::publication::allowlist::allow_phase1_publication_artifact` is the +additive durable-publication gate over that sealed artifact. It consumes the +artifact and returns an opaque allowlisted typestate carrying exactly one of +the same seven leaves: supporting Profile or one of the six root publication +forms. The companion +`allow_phase1_publication_canonical_json` operation reloads persisted canonical +artifact bytes through the same gate. The gate revalidates the artifact, then +reprojects kind `1` with Ask-before-PhotoUpdate-before-Update precedence, +requires the strict typed date/time NIP-52 profiles, and marker-partitions kind +`30402` before accepting focused FoodAvailability. Raw or generic events, +including raw date/time events, replies, comments, deletion requests, calendar +collections and RSVPs, BUD-11 authorizations, ephemeral events, trade, +commerce, groups, and operations cannot produce the sealed input and fail +closed at canonical reload; deferred route/delivery product surfaces are +non-events and likewise fail closed. This allowlist does not change +event-contract registry v7 or grant signing, media readiness, transport, relay, +or entitlement authority. + Persistence crosses only the explicit `to_canonical_json` and `from_canonical_json` operations. Schema version 1 orders the envelope as `schema_version`, `semantic_variant`, `authored_operation_id`, diff --git a/crates/event_codec/contracts/phase1_publication_allowlist_v1.descriptor.json b/crates/event_codec/contracts/phase1_publication_allowlist_v1.descriptor.json @@ -0,0 +1,24 @@ +{ + "contract_id": "radroots_event_codec.phase1_publication_allowlist_v1", + "event_contract_registry_v7_sha256": "91595544310f865bdef064ee760c227c870417a95b87b3e27278f8da74fdddea", + "manifest": { + "byte_length": 86551, + "hash_algorithm": "sha256_bytes_v1", + "path": "crates/event_codec/contracts/phase1_publication_allowlist_v1.manifest.json", + "sha256": "5ba2e7d6cdd616b423d3bdbcf2c6d2644d4f2568b1eb13cbaacf101a076f485c" + }, + "manifest_schema": { + "byte_length": 8439, + "hash_algorithm": "sha256_bytes_v1", + "path": "crates/event_codec/contracts/phase1_publication_allowlist_v1.manifest.schema.json", + "sha256": "5bdccda1449ca6837d9922171983b880e81324cb0bdefc32e1d92b387f692a4f" + }, + "manifest_sidecar": { + "byte_length": 65, + "hash_algorithm": "sha256_bytes_v1", + "path": "crates/event_codec/contracts/phase1_publication_allowlist_v1.manifest.sha256", + "sha256": "3741e277ad9b80a00a611be5ecbb49419e61bd7949b0efa02727cd60f58b81f7" + }, + "predecessor_manifest_sha256": "0776e1d84c9366954047e75cdf12d9acc9a7108260157c3534f22067075a385a", + "schema_version": 1 +} diff --git a/crates/event_codec/contracts/phase1_publication_allowlist_v1.manifest.json b/crates/event_codec/contracts/phase1_publication_allowlist_v1.manifest.json @@ -0,0 +1,2307 @@ +{ + "schema_version": 1, + "contract_id": "radroots_event_codec.phase1_publication_allowlist_v1", + "authority_id": "phase1_publication_allowlist_v1", + "manifest_schema": { + "path": "crates/event_codec/contracts/phase1_publication_allowlist_v1.manifest.schema.json", + "byte_length": 8439, + "sha256": "5bdccda1449ca6837d9922171983b880e81324cb0bdefc32e1d92b387f692a4f", + "hash_algorithm": "sha256_bytes_v1" + }, + "predecessor": { + "contract_id": "radroots_event_codec.phase1_publication_artifact_v1", + "immutable_artifacts": [ + { + "path": "crates/event_codec/contracts/phase1_publication_artifact_v1.manifest.json", + "byte_length": 89464, + "sha256": "0776e1d84c9366954047e75cdf12d9acc9a7108260157c3534f22067075a385a", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "path": "crates/event_codec/contracts/phase1_publication_artifact_v1.manifest.schema.json", + "byte_length": 11972, + "sha256": "1d72cee2754e7ac45105d79b1ecf7d44251991be7a18ba106166e962000e8320", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "path": "crates/event_codec/contracts/phase1_publication_artifact_v1.manifest.sha256", + "byte_length": 65, + "sha256": "586c0985b502f22241b4d90f2ecb475d43953fc3ddd66d6fbfa3b3eb9cf34444", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "path": "contracts/conformance/vectors/publication/phase1_artifact.v1.json", + "byte_length": 23113, + "sha256": "ec18c687d5b0710a48624ddb620d89157e6b645dbea8bb91c62e3a111d20c622", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "path": "crates/event_codec/tests/fixtures/phase1_publication_artifact.v1.json", + "byte_length": 23113, + "sha256": "ec18c687d5b0710a48624ddb620d89157e6b645dbea8bb91c62e3a111d20c622", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "path": "crates/event_codec/tests/publication_artifact.rs", + "byte_length": 34582, + "sha256": "7a31169eac4217a38cb3ef25eb9213f2f89e11fb17e76ceaf7449b34225e98af", + "hash_algorithm": "sha256_bytes_v1" + } + ] + }, + "event_contract_registry": { + "version": 7, + "inventory": { + "path": "contracts/event_store/event_contract_registry_v7.inventory.json", + "byte_length": 158021, + "sha256": "91595544310f865bdef064ee760c227c870417a95b87b3e27278f8da74fdddea", + "hash_algorithm": "sha256_bytes_v1" + }, + "sidecar": { + "path": "contracts/event_store/event_contract_registry_v7.inventory.sha256", + "byte_length": 65, + "sha256": "823081e6f423c149865521b185a8517aea35ce4470fa77c165ad7ca5207f036b", + "hash_algorithm": "sha256_bytes_v1" + }, + "evolution": "immutable_registry_v7_plus_additive_phase1_publication_allowlist_v1" + }, + "allowlist": { + "version": 1, + "contract_id": "radroots.phase1.publication_allowlist.v1", + "operation_id": "publication_allowlist.allow_artifact", + "canonical_json_operation_id": "publication_allowlist.allow_canonical_json", + "input_type": "RadrootsPhase1PublicationArtifact", + "output_type": "RadrootsPhase1AllowlistedPublicationArtifact", + "allowed_leaves": [ + { + "leaf": "profile", + "kind": 0, + "publication_role": "supporting", + "authored_operation_id": "profile.build_authored_draft", + "event_contract_id": "radroots.profile.metadata.v1" + }, + { + "leaf": "update", + "kind": 1, + "publication_role": "root", + "authored_operation_id": "social.update.build_authored_draft", + "event_contract_id": "radroots.social.update.v1" + }, + { + "leaf": "photo_update", + "kind": 1, + "publication_role": "root", + "authored_operation_id": "social.photo_update.build_authored_draft", + "event_contract_id": "radroots.social.photo_update.v1" + }, + { + "leaf": "ask", + "kind": 1, + "publication_role": "root", + "authored_operation_id": "social.ask.build_authored_draft", + "event_contract_id": "radroots.social.ask.v1" + }, + { + "leaf": "event_date", + "kind": 31922, + "publication_role": "root", + "authored_operation_id": "social.calendar_date_event.build_authored_draft", + "event_contract_id": "radroots.calendar.date_event.v1" + }, + { + "leaf": "event_time", + "kind": 31923, + "publication_role": "root", + "authored_operation_id": "social.calendar_time_event.build_authored_draft", + "event_contract_id": "radroots.calendar.time_event.v1" + }, + { + "leaf": "food_availability", + "kind": 30402, + "publication_role": "root", + "authored_operation_id": "food_availability.build_authored_draft", + "event_contract_id": "radroots.food.availability.v1" + } + ], + "kind_one_precedence": [ + "ask", + "photo_update", + "update" + ], + "event_policy": "strict_typed_nip52_date_or_time_artifact_only_v1", + "classified_listing_policy": "raw_marker_partition_before_focused_food_profile_validation_v1", + "denied_families": [ + "unsealed_profile", + "unsealed_calendar_event", + "generic_root_note", + "reply", + "comment", + "deletion_request", + "generic_calendar", + "calendar_collection", + "calendar_rsvp", + "bud11_authorization", + "ephemeral_event", + "operational_listing", + "generic_nip99", + "mixed_classified_listing", + "trade", + "commerce_order", + "route_delivery", + "group", + "operations" + ], + "granted_capability": "phase1_durable_publication_lane_entry_only_v1", + "excluded_capabilities": [ + "signing", + "media_upload", + "media_retrieval", + "media_readiness", + "relay_transport", + "product_entitlement", + "inbound_registry_admission" + ] + }, + "predecessor_source_supersessions": [ + "CHANGELOG.md", + "contracts/operations.toml", + "contracts/releases/1.0.0-alpha.1.toml", + "crates/event_codec/README", + "crates/event_codec/src/wire/publication.rs", + "tools/xtask/src/contract.rs", + "tools/xtask/src/contract/phase1_publication_artifact.rs", + "tools/xtask/src/main.rs" + ], + "source_files": [ + { + "role": "cargo_config_authority", + "path": ".cargo/config.toml", + "byte_length": 37, + "sha256": "7cf5642012b512304c8b503e13fba165873fd33572c1e97f4109ae8796def384", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "release_notes", + "path": "CHANGELOG.md", + "byte_length": 30938, + "sha256": "45fdb845ae0f231cdb0e94ce49c472dab64ff4bbecc79d751c23a3d813708932", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "workspace_lockfile_authority", + "path": "Cargo.lock", + "byte_length": 216965, + "sha256": "f26bf62f77e48914c89c15e689fdbc6799928e9603cab38f50c0c65a0c405edf", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "workspace_manifest_authority", + "path": "Cargo.toml", + "byte_length": 10836, + "sha256": "285532dbb0894204843a832880f136ceac5ee312a3203ff951fb0551fac63ec4", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "operations_authority", + "path": "contracts/operations.toml", + "byte_length": 78556, + "sha256": "3c96d953e208b1781b391a722414861a859c106101ccf8d7b7789193f197d7eb", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "release_authority", + "path": "contracts/releases/1.0.0-alpha.1.toml", + "byte_length": 20917, + "sha256": "42c472fd5927d976c108f4d7204da2d34ff13de2066fa36d3b2e5f038a274df4", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "blossom_manifest_authority", + "path": "crates/blossom/Cargo.toml", + "byte_length": 901, + "sha256": "9e84a9f1820e84994e5c8f705cae243eadd4676244f98604c53fc0800f0e51b3", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/blossom/src/authorization.rs", + "byte_length": 44288, + "sha256": "319eb9a5de6389c539d164a985900730b886685ebcac0382b803d8249a38bef6", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/blossom/src/descriptor.rs", + "byte_length": 16107, + "sha256": "64bc09a878cabd5ea327f8b86a1c18a7d1e6a5e686c0441c899a5d0790702532", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/blossom/src/error.rs", + "byte_length": 17227, + "sha256": "8b0b9557cca68604791c9bb080f3cb0ff81a2397efb7f1ab600b2b4f4f7f8fa7", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/blossom/src/hash.rs", + "byte_length": 11251, + "sha256": "753eff6ef9a810045c88839d39a46b37d62a4ad0a5ea57aa30e2a8219e3cdbda", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/blossom/src/lib.rs", + "byte_length": 1335, + "sha256": "61ebee86f02887c45507bab052686da082f74ae26f23d18ff4019e02c70097bd", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/blossom/src/url.rs", + "byte_length": 15364, + "sha256": "05325325da6627ba48318851fb4e61a9ad540aad37fc1c27ac9a47f27300891f", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "core_manifest_authority", + "path": "crates/core/Cargo.toml", + "byte_length": 982, + "sha256": "2f386a9a0a87acfcbd8e39dc4c78af30d0d6c3855689194ff0355d8d730713c9", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/core/src/currency.rs", + "byte_length": 4261, + "sha256": "94f4ac6ef4ac0957c86b66a4910390b36c09faad9c05162a64220969cf263861", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/core/src/decimal.rs", + "byte_length": 3895, + "sha256": "84c4fcd5d0979c5c931ac84096b8eb35d9ac842d853df07af5da42aa2c813105", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/core/src/discount.rs", + "byte_length": 2439, + "sha256": "66406ccfc3662519d6bcf7737818e39ab8cbb322f384cd3cfbbabf7f7a8c3fb8", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/core/src/dto.rs", + "byte_length": 1151, + "sha256": "77a2ed8df26459f117262ef86f93f80718a54eedbdb583ffd55f25517743e32b", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/core/src/generated/dto_roots.rs", + "byte_length": 1163, + "sha256": "a611e8712acd07098bd088f050f4112293b5014c574e4211406dc12dcb90b57a", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/core/src/lib.rs", + "byte_length": 1177, + "sha256": "56ae7544e668488533ede4a9573d710bed5f1e125811862d82bc6ba914854041", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/core/src/money.rs", + "byte_length": 6812, + "sha256": "2194a9777d8684a751345236afe48647176b451fb695e89fc13f2f18dd030055", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/core/src/percent.rs", + "byte_length": 2373, + "sha256": "d109bae54bfb9469134e044064406d10c395ea9d3604eda1ca8ab10b83ae9575", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/core/src/quantity.rs", + "byte_length": 6698, + "sha256": "668ee66ec23917c74d2d5ef494f966cbbfe7417a4f17eb1a239c49d86dfa89e8", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/core/src/quantity_price.rs", + "byte_length": 6207, + "sha256": "0f8945e17638ba5dc6da6bb127f4e35b67433354dad8645c5b958262bdf32eed", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/core/src/serde_ext.rs", + "byte_length": 755, + "sha256": "fe2866f1d0aa659dd4f5a45c7c30fd0e509993112ef5fa7b1502c2d88d92249c", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/core/src/unit.rs", + "byte_length": 10255, + "sha256": "cf248951bf90bcdf75121a8e4dca6415ffb976ff4eead7c453f24bdb700fbf1a", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "event_manifest_authority", + "path": "crates/event/Cargo.toml", + "byte_length": 1711, + "sha256": "e6e9b57dde0f3cea4a67adfe128ecfbf5ac727488efdf819323a8008bb2b2fa6", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/account.rs", + "byte_length": 539, + "sha256": "ae3d3ed5bf8096c35fb065b1cd35bd281a7dd6d10085df235e65fb1b178ef1fa", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/app_data.rs", + "byte_length": 506, + "sha256": "cea3f71921f5a5e092024715b6e9b07aff11dfb6f28bd83a0998e293509c29ce", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/article.rs", + "byte_length": 2247, + "sha256": "99b0ab0f70b46415685f2d5de73010ff027335194d0a7acbed15bee337f0e167", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/calendar.rs", + "byte_length": 103971, + "sha256": "e6f090246aecc94cb107eef8951e926324d7216b30bc83dcf8a6ad2e6bd508d6", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/classified_listing.rs", + "byte_length": 8226, + "sha256": "745aa08953a6243ea1afad22aa63e44f6746f2f243ab0d6b617d6ab64fa58749", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/comment.rs", + "byte_length": 36170, + "sha256": "5bfd3c8ddf77ef77b61eebb0c50f1d3384e7724a55d18f1b2c06bbb32b5c7c65", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/contract.rs", + "byte_length": 86, + "sha256": "9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/contract/registry_v7.rs", + "byte_length": 155836, + "sha256": "7100b12838e8a65e5a0e8152c999efbafef38d54944cbb2edc4c3ceedc1c1b55", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/contract/registry_v7/tests.rs", + "byte_length": 67724, + "sha256": "113b4e7231abc5a00e7d68214aec09ad44be2c839ecaeff9678e4d34f7487241", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/coop.rs", + "byte_length": 1400, + "sha256": "15ca2fba78362fde9e2a6b584880112737302d2cc666102159b4cc1bd3044415", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/deletion.rs", + "byte_length": 31019, + "sha256": "d04ac21d395e3607b61aa1b0ec86af8e114fbe4b3ebb6fbb596bcb51a0354996", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/document.rs", + "byte_length": 974, + "sha256": "a7af3be1a948c7be163b7e720cdbfb20d60142b3def0f2e31ca391a054dd1419", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/draft.rs", + "byte_length": 59007, + "sha256": "0bd517ba0969ca253584c6bda4b08494b63012463a259750ebfb81443a5ce4bd", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/dto.rs", + "byte_length": 5222, + "sha256": "72272da20bd529ef89a1c7e761f94d53f9e1a333ab1f12bee8f3fe7ee7d0b900", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/envelope.rs", + "byte_length": 31243, + "sha256": "3a9bd5cc07ef4573ae132d848cea5d3f18f61137b132672f9088cbe1a917f6a2", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/event_head.rs", + "byte_length": 68, + "sha256": "cb52f6006f7ecd862707d6b048f9fc407e0cd5ebcd3091b3c54e195ffa5cba64", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/event_head/v1.rs", + "byte_length": 7574, + "sha256": "fb52339def559f01f43f2f02dd142da192cd7b7074e116783f56a2b6009fddfc", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/event_head/v1/tests.rs", + "byte_length": 12218, + "sha256": "25aea7d0f9bc170c5f94d786ac855dc7373c76e17e755cf42baf454f6f9fdfa6", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/farm.rs", + "byte_length": 1341, + "sha256": "52c67d44ebcfb4c9716fb4e47bf4268b0a1734c2255fb6bfd2fcd5912ec06aba", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/farm_crdt.rs", + "byte_length": 23010, + "sha256": "50f629a6b98063ae02fdd2f10d7e6bcb89908815b223dceb7cb27d9d8501aa51", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/farm_file.rs", + "byte_length": 4988, + "sha256": "11cc5fbe20d7cb3a1d656d62448ce5d343a6c65c34e0119d714ac905cecc16be", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/farm_workspace.rs", + "byte_length": 5441, + "sha256": "15439d472f89cf9b2c37de035d9ba24a0c74498b0fb3704f6766030b5ea60c9f", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/file_metadata.rs", + "byte_length": 3567, + "sha256": "e58477754386bd5029ed97ce37f5459613692abb9fc484432682103d4583eca7", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/follow.rs", + "byte_length": 844, + "sha256": "ef622d7d549e6566d632931bf63b0cca2a775328bb708df345410f586111f170", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/food_availability.rs", + "byte_length": 45680, + "sha256": "1c8c3fba6514f9b246545b3305e8ac2742258857d1b813fa34c5fbefa6135e12", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/gcs.rs", + "byte_length": 1752, + "sha256": "c2769982fb3798922998fb4c73eb9bc24809f00d2e274157eda9b2512458d69c", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/generated/dto_roots.rs", + "byte_length": 11201, + "sha256": "bba7b3af6a4bde9479ca640923a849665739a238e5019f0e3f68e462b99161ca", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/geochat.rs", + "byte_length": 458, + "sha256": "d8f3393aeacd335adb06a8c292e0cf183b352d1e14ad7ed7538c5ccc651d891b", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/gift_wrap.rs", + "byte_length": 793, + "sha256": "fac8c4e9ae3704228d41bd0b6d5220a8f2ab2406c73f6683514f1cc8dd0684bf", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/group.rs", + "byte_length": 10479, + "sha256": "ce7576787aba1c205b0486dd941efb7c9870b78fb969176c0e2a4f292cc80fab", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/http_auth.rs", + "byte_length": 1614, + "sha256": "81f9bd16ecd62f5ca2f5b3f9101a8abc479c71f33ec1202a28ab505d883b94b3", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/ids.rs", + "byte_length": 45525, + "sha256": "9f6a893d1f9523157e70cf5a0fd5fde8221c7a518a130b2924de67c04f8eecb2", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/job.rs", + "byte_length": 1887, + "sha256": "99a4a1926a50a56c1da40721332abc7ff34b79809255f2d13727ed82b9ddc597", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/job_feedback.rs", + "byte_length": 704, + "sha256": "c19334328646115d10d66fb19bd5c8082e20363b0f62384dc7c3fee861b41c89", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/job_request.rs", + "byte_length": 1387, + "sha256": "40080d798b75a127e304ea93c89cd6e471a2c6139745b94708dd50bcaeb050a7", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/job_result.rs", + "byte_length": 719, + "sha256": "f8abb7d5f832f1e86a3b1b5ad20b0e5f8099902307850c04b17702a709ff0b21", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/kinds.rs", + "byte_length": 34961, + "sha256": "8b3ce6193cab1f7e1587d0c1b2880a81b0aa43c77d89671b602772db37edcebf", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/knowledge.rs", + "byte_length": 56561, + "sha256": "f043b448f781d10330e065e6b3df77f9000434a02745d2590f839fca6e7a6649", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/lib.rs", + "byte_length": 2746, + "sha256": "ae2a43690d44017565dfbc86af5033a60cdb39a5f32440956c8e0325accc5120", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/list.rs", + "byte_length": 1636, + "sha256": "0bbfde90ac3ea769ae9b603f78654a8dd6f436e35cab88f86410e10946d30d59", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/list_set.rs", + "byte_length": 560, + "sha256": "84b1227413967f8aa491996527469a9bd83f2186b294c20415f26e9116d5a537", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/location.rs", + "byte_length": 1932, + "sha256": "f2fcf688c66e9798c20cf7737d1f9d0c4f9bd030cbfeedc1375c6c6fdfd2d120", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/media.rs", + "byte_length": 3821, + "sha256": "ca98ddb134cb522912900544080d910867398d6aff6e8c6b061dc1fb77212ec3", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/message.rs", + "byte_length": 882, + "sha256": "b8d513067218a4848d67f316708448549871608752b666ccae87adf1652c912a", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/message_file.rs", + "byte_length": 1380, + "sha256": "b4f0b4f9a641a78f4400bdbbe7c857d814cf5d20cfbb56aca1f09e80da975473", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/operational_listing.rs", + "byte_length": 10125, + "sha256": "34ff16458fd9d5e19e80b49fcf80f41903081f19056ab4e50ea4af6665af3acf", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/order.rs", + "byte_length": 64764, + "sha256": "edaf3e270dcffa6ab33bba206fa0aaf4480f093f5e03b97b70af0543e78ae96e", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/order_economics.rs", + "byte_length": 5749, + "sha256": "f4c05cdcdeea77f88052b56807d687b16b2a1c8cf60f0aef9b5770d194678240", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/plot.rs", + "byte_length": 1333, + "sha256": "47cef6cee21d3ea915f1eb4ebe47d15a70270801e7b49b05d91def6062a34142", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/post.rs", + "byte_length": 22813, + "sha256": "d4a573cf2510f3a261f6aac3efd16489591281cdbdb387af5a7d67f516e1e056", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/profile.rs", + "byte_length": 19061, + "sha256": "f2b03be027aea187645e8bc3ae7a7d4004d6e58d4e1b361b35786ccdc36d907a", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/reaction.rs", + "byte_length": 429, + "sha256": "ad0ce1e9bef6b0d20665c0c69bfee81819136786b8f3cee468815b9ba70b95cd", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/relay_auth.rs", + "byte_length": 1099, + "sha256": "1c4ae98333a22889f76d2143167054fb48265e0148a241fcd9c5fd549836ba9f", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/relay_document.rs", + "byte_length": 1394, + "sha256": "5e0abdcb0d39f5bbebce331fd96c64678cb1852e195e82481732f6057aad7b1c", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/relay_hint.rs", + "byte_length": 14317, + "sha256": "1a14aea5d2f2620970dc1de9e3d311d9042cab4d5805a56461548168112a0801", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/reply.rs", + "byte_length": 16026, + "sha256": "07979efc36b8096984c08908655ea7332aca9a9bc28857a4e5adda28f704d363", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/report.rs", + "byte_length": 2274, + "sha256": "5324a57e4cebc8da10ebb746ab91b216d473421465c860a0cd2e3b4396929d77", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/repost.rs", + "byte_length": 1942, + "sha256": "e56ce06bb11d46eb579f33561d6bafe0e30d44922f34c58708ba0cc05363edf7", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/resource_area.rs", + "byte_length": 1326, + "sha256": "a3bfa00639b1c5408e7508da1e331f37554ad303adb12ed2d9257aee0c470859", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/resource_cap.rs", + "byte_length": 1354, + "sha256": "a8b0eac266c7b85fa6a86e54c5ce85de75e813498ab19df269abc997c09f0d33", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/seal.rs", + "byte_length": 370, + "sha256": "1cfcaff0e7cc310aa4c903b35260efbec53c81080f8ad5bdf57e03375f7bd673", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/social.rs", + "byte_length": 5468, + "sha256": "e619ac41041c7e03d14fff95664cd3ec77be181ce9954d76c28daf1fcc28b3d2", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/tags.rs", + "byte_length": 4530, + "sha256": "cb8f12a639fc72d9238e32495d9e67e928e6627fb8c94ec0a58c5d95e7373cd6", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/trade.rs", + "byte_length": 68364, + "sha256": "33f34933a75459314c721ca65631a0a7b2493439bd638a10ee3e103d0845fbfe", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/trade_validation.rs", + "byte_length": 4794, + "sha256": "430ebb19ff1399e09213f720521e5f9ec467c63662aa25e870584486079d6b61", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/wire.rs", + "byte_length": 68, + "sha256": "cb52f6006f7ecd862707d6b048f9fc407e0cd5ebcd3091b3c54e195ffa5cba64", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/wire/v1.rs", + "byte_length": 22136, + "sha256": "4c65e240be61246bae91ca3223ec93fe8efdd66324e8c198207fd993896044d7", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event/src/wire/v1/tests.rs", + "byte_length": 18595, + "sha256": "7b31012f1033af75b77c61ca9895e52f660e206a103fe8cf5a84cfbe00cb82f4", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "event_codec_manifest_authority", + "path": "crates/event_codec/Cargo.toml", + "byte_length": 1772, + "sha256": "e0c227e10a3bc2aedd6f3ea676dff0100b5ac52847df01a404cf2ac7c5d11a6b", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "event_codec_documentation", + "path": "crates/event_codec/README", + "byte_length": 22543, + "sha256": "8b09d3b8e39387b10a4a676fe2c5ed7d56cc31af79bdb8edf88f66a8f65e2c69", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/admission.rs", + "byte_length": 20666, + "sha256": "aa51f9e571eb80ade307cf9069a42ac7adfdc4c4abe5da1a2f6d4a7192c7ceea", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/admission/registry_v7.rs", + "byte_length": 5350, + "sha256": "755e63dd7ad1115c219e5a3ea540bef67d2770fc9f2a5aa6ba97c925c99bdced", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/admission/registry_v7/tests.rs", + "byte_length": 834, + "sha256": "d24359ab6004a316725ccfe448f2cf4e422f918ad4128e37358fa84bee069a0f", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/app_data/decode.rs", + "byte_length": 2058, + "sha256": "60cf53efb646c04dd2927b3f5e6fa637f2b856af756f9e79d49b6b3743cdc7b6", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/app_data/encode.rs", + "byte_length": 1150, + "sha256": "529c6d828ebfe2d0f0b629c94983f3f5577629069d8a5cbc6b3b49355aa538f4", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/app_data/mod.rs", + "byte_length": 57, + "sha256": "5c57058e78fa54008ba63a97ba12aee00db28753fa39bdff3cb7347e3447dabc", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/article/decode.rs", + "byte_length": 3346, + "sha256": "ace004ba624b3e69629f9098b18400c42eb4302b248742032a46b97397b52dfc", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/article/encode.rs", + "byte_length": 2328, + "sha256": "b3c67259750aa38431c2a7e17c9baf4a4db6703e13ed9af2df897c4b0c10a1dc", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/article/mod.rs", + "byte_length": 32, + "sha256": "f72b49a09f84d980791360980c31534c1058bd90ba181ba7e97d871663998c0d", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/calendar/decode.rs", + "byte_length": 23117, + "sha256": "9416fc8b061d230ab5c36877f70a6668fc75a171ac2005286a7cc21878fcd62e", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/calendar/encode.rs", + "byte_length": 10162, + "sha256": "fa71aed54f24c003bffb7b860b0864033b1f3c9cbc87ab10b04e346c92ee55c4", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/calendar/mod.rs", + "byte_length": 32, + "sha256": "f72b49a09f84d980791360980c31534c1058bd90ba181ba7e97d871663998c0d", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/comment/admission.rs", + "byte_length": 4176, + "sha256": "32eb486d43790a92d2faeed6684396f77aef33061d4a56bd848e0925c5c385f0", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/comment/authored.rs", + "byte_length": 11756, + "sha256": "ff93c671d3053716abe9cc0db849c61eeabffcf19de10169d94f7440ddfc574b", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/comment/inbound.rs", + "byte_length": 86, + "sha256": "9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/comment/inbound/registry_v7.rs", + "byte_length": 53887, + "sha256": "e540649f129901fa262e15a5ee9318c4ca70dbd8ec6ff0ff62a4ddb94750a225", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/comment/inbound/registry_v7/tests.rs", + "byte_length": 11799, + "sha256": "afb4ccd66aeabc7029246241f107b7f729a03c399f0c8e274c3e2530b090812c", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/comment/mod.rs", + "byte_length": 54, + "sha256": "986f59844194fbaac78618c6131940691d49dc888702366b3520c901a08990e0", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/coop/decode.rs", + "byte_length": 2512, + "sha256": "70ed255179e781417a59d4d2567cd88fe488ccd1eaca6b6e2df8bebee6c7f969", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/coop/encode.rs", + "byte_length": 2820, + "sha256": "19ac6c780f27eb691375dbcd438f4b24037f95ef7af05c3ccfdc67542ca25bba", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/coop/list_sets.rs", + "byte_length": 11371, + "sha256": "d626bd31d764c0eba3ded920f6973de2eb18e56e92ce747068e5a067cbc41fe3", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/coop/mod.rs", + "byte_length": 7095, + "sha256": "1a78f8e91d88d754266798c8312801a0f910fb146d28433aa0311b2c3cceff5f", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/d_tag.rs", + "byte_length": 1880, + "sha256": "65d41e1b2e799fa036a32f587176d6f321319cff2bdc469875f009014e4b96c6", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/deletion/admission.rs", + "byte_length": 73, + "sha256": "2a652ac0ad4f1b99078b7dc361bc2d2e0dc29a1f0343aedb35a689a5388fd3f6", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/deletion/authored.rs", + "byte_length": 6260, + "sha256": "3d103b07afa12119771d87326d10dc8f2393abda96b88146c0e87d5c05c71471", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/deletion/evaluator.rs", + "byte_length": 73, + "sha256": "69b00d69fbae5bd5c1384c5ac926982656ffea34f826f5affb75ddd5672bb333", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/deletion/inbound.rs", + "byte_length": 71, + "sha256": "e502eccc04cf9c53aa54372cc48e1521e2de42bf58669f653b293bb10eccd00e", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/deletion/mod.rs", + "byte_length": 115, + "sha256": "17d56f82ddb0a86bc97abbcf3e037fe460ea31fff2548f527be4bf004dac3a95", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/deletion/reconciliation_v1.rs", + "byte_length": 38491, + "sha256": "a3e23142c65c5c972b117fb65492e1e49755c3ada95ced4421161e0748116d42", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/deletion/reconciliation_v1/admission/tests.rs", + "byte_length": 2292, + "sha256": "50bdf2ca5a2a63ca84afa4485554045237a91aa13cda1cc0fcb9cf9e02dddbc0", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/deletion/reconciliation_v1/evaluator/tests.rs", + "byte_length": 16583, + "sha256": "0c33fb1eb3ece09889f1d9f25d042a2012d851b462a94ee56619e96bc2dac6e2", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/deletion/reconciliation_v1/inbound/tests.rs", + "byte_length": 14354, + "sha256": "c7bef0de70e98c0535aba83b4be0840b912c14ac11ec9a1156be35a451a2dfcc", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/document/decode.rs", + "byte_length": 5731, + "sha256": "986f0333047ade2a0b1fd598c3498a4170121bd122ec888d7ac19d7af5d7c457", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/document/encode.rs", + "byte_length": 4577, + "sha256": "5224d82236b99b46b67708435d56a08ef988b6bf109778b0b16925f6a69bfbb4", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/document/mod.rs", + "byte_length": 1266, + "sha256": "6f4acb5c28649aa62a8f69cc2d67c8dbc012fd98f307ba02efed9d2879d64c37", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/error.rs", + "byte_length": 3668, + "sha256": "786e1fee6e0c181610cac49e26d313ee99ae995968aedf29090d829c738b3731", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/event_ref.rs", + "byte_length": 5841, + "sha256": "ba6eb777bb904abe0af5cf9e7cbab80e9a17a951eeac9a11b3f5a0a1ee1c0f38", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/farm/decode.rs", + "byte_length": 5353, + "sha256": "d81164d8d94116b68009875fa20f7473ad0fe47bd61c5aada8c058377996008a", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/farm/encode.rs", + "byte_length": 3262, + "sha256": "20cea8aa2d56c3ede56b9539409f3b650d03310897b08eb8bdcfd3c0e0ee632c", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/farm/list_sets.rs", + "byte_length": 9511, + "sha256": "dc2e9e2b5a3521dbd5ebb8c0ee7e12ceeab94aa81fc0bb1382d5d5359b52c27a", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/farm/mod.rs", + "byte_length": 19663, + "sha256": "70e1ebf5d9432288d3ab6dc2c1491eecaaa52d352fa7888ba0e662a96fa88490", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/farm_crdt/decode.rs", + "byte_length": 5310, + "sha256": "030fbb031a6eb103fbee8c67778af866e8dec61d5599a6934b002f9ff9b299ed", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/farm_crdt/encode.rs", + "byte_length": 4298, + "sha256": "1c7ed28ca74a5b4722cb159e89e21bc78776e8a87258a1c8973caae9f0c25277", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/farm_crdt/mod.rs", + "byte_length": 25842, + "sha256": "9f46145cef77562e174dce30dcd16ec843fd519c2172961ab9e21f45dad715be", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/farm_file/decode.rs", + "byte_length": 10354, + "sha256": "d6858907bb2d5540c63e2a1a3b82763534189adf940270e2db1e0d974282543d", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/farm_file/encode.rs", + "byte_length": 6395, + "sha256": "bc1194186acfe6a53ed43f3fb9c907e8a15500e5a14955262fc5890e54d5d4a8", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/farm_file/mod.rs", + "byte_length": 18929, + "sha256": "a1fdfcdd010887b319000a487371aba5b318a2efc477b7ade80615d664f9ef8c", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/farm_workspace/decode.rs", + "byte_length": 4739, + "sha256": "11f999e3f49b39e83882e789f9273d34051268f836705c68815a46acd0da50cd", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/farm_workspace/encode.rs", + "byte_length": 3638, + "sha256": "4c60fc3fe7639e4d48e10b2b1ac49375ce49683e8d6fcf6e83273e5f5894fadf", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/farm_workspace/mod.rs", + "byte_length": 19261, + "sha256": "a29f42b0f4dd4e5330bf7b29f0877165f4509f173e3d564925b1cbc87e8ddcf8", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/field_helpers.rs", + "byte_length": 11763, + "sha256": "abbb5b777e6b6b2b58aadcccab27647ba830c287f503c27fbfe42998a33e4d6e", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/file_metadata/decode.rs", + "byte_length": 5033, + "sha256": "62573603394d8586a050c2e443185b079e4d56c22d58e8a04b41489b159c43f8", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/file_metadata/encode.rs", + "byte_length": 3753, + "sha256": "6abf5c981e77235ec3c454ad1be9be745ab1448da07a53074d3fbebc73699372", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/file_metadata/mod.rs", + "byte_length": 32, + "sha256": "f72b49a09f84d980791360980c31534c1058bd90ba181ba7e97d871663998c0d", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/follow/decode.rs", + "byte_length": 2726, + "sha256": "912fbb67053d766c2ff78993eeed05e9b72a0ea8c7424ec1d83026060306dce0", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/follow/encode.rs", + "byte_length": 5928, + "sha256": "ab3245bca4132ea129db60206aff9f1b510d0a633704da4c2bee21c3a31f234e", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/follow/mod.rs", + "byte_length": 32, + "sha256": "f72b49a09f84d980791360980c31534c1058bd90ba181ba7e97d871663998c0d", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/food_availability/admission.rs", + "byte_length": 5799, + "sha256": "727ebf8f086c14376aba745a0a02b269115fed01e8ce95f90fdd211b9640f842", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/food_availability/authored.rs", + "byte_length": 10125, + "sha256": "57c4c4342a6c37f1d02fb16384396274e2bf0f7f588321564bbb918c0d498822", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/food_availability/inbound.rs", + "byte_length": 86, + "sha256": "9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/food_availability/inbound/registry_v7.rs", + "byte_length": 26865, + "sha256": "908015346eaec97bee6f44fe2a524410b701529ac19a5dab4505706d40d527da", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/food_availability/inbound/registry_v7/tests.rs", + "byte_length": 6112, + "sha256": "4c5293e22009a768abfbba056686b15ad07558495cc83b071b979e269bb37637", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/food_availability/mod.rs", + "byte_length": 72, + "sha256": "7ef96c5ac35a698bcc02bc8b083a5972aaceb9a0e72ba085e68f3de2639f10f8", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/food_availability/revision.rs", + "byte_length": 3881, + "sha256": "62cee7fe83f9f9ccf98981cda5fab63b70e98736b16f7afbee5e2c5725ef5e7c", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/geochat/decode.rs", + "byte_length": 3357, + "sha256": "e8ae9f749812fa2d712cb15c17638379e67dd49ae60ec347712ddbf0efa13fe0", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/geochat/encode.rs", + "byte_length": 1799, + "sha256": "6ecbca2ba32d9b8d91efa381dbd8ead122320dcdf421199287a964cfc15bc89d", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/geochat/mod.rs", + "byte_length": 57, + "sha256": "5c57058e78fa54008ba63a97ba12aee00db28753fa39bdff3cb7347e3447dabc", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/gift_wrap/decode.rs", + "byte_length": 3125, + "sha256": "0ccea7238978d14a669f2f39c76b353c342559e2c8ca9e9d325fa5174f535d92", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/gift_wrap/encode.rs", + "byte_length": 2051, + "sha256": "86d34554d225a77cb0bf625d32be25b3233f8267c693cb0551c62ac5d595caff", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/gift_wrap/mod.rs", + "byte_length": 57, + "sha256": "5c57058e78fa54008ba63a97ba12aee00db28753fa39bdff3cb7347e3447dabc", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/group/decode.rs", + "byte_length": 10828, + "sha256": "9bb5d7898025f7a8dabeb223c721b33ad16f4faadf30793cc1a862fd849c1480", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/group/encode.rs", + "byte_length": 11784, + "sha256": "4565d50cc4ca225983283a5ae7fb0465b64dc8302979686ea3669dcbdd13cf1b", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/group/mod.rs", + "byte_length": 24171, + "sha256": "a6356251c7bfe0ae16555e97ac029adde2a22401fbb3ed3da776d3c463ca2cd8", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/http_auth/decode.rs", + "byte_length": 2106, + "sha256": "fb9b0912f9d467c0b34d3293f67347333eac3cd645b30409c512d28e82f1bd52", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/http_auth/encode.rs", + "byte_length": 1530, + "sha256": "292c687b0b4df4bb331d4dca8804f2f058fddd334cdc3b0973078b0b006cf237", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/http_auth/mod.rs", + "byte_length": 8207, + "sha256": "9eedfd01a4ee7e44e1be80dd497cc0f82f6399bf6c8baa6f7d44933f0665f16f", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/job/encode.rs", + "byte_length": 1676, + "sha256": "75b1f0bce46524d2a1a0e7bffabbf86d08518d7ee4c69efd44b4a23881562651", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/job/error.rs", + "byte_length": 1578, + "sha256": "aa4deae1ae26130ef092b5863309c9b070b00fed98855b0bab2d0cc136feb915", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/job/feedback/decode.rs", + "byte_length": 3261, + "sha256": "0aec9d35ce4d4879c6ad2d6cdee9cf743642c0d74594a6dd481a7e10352b653e", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/job/feedback/encode.rs", + "byte_length": 1853, + "sha256": "00a48ba2cda580fcfd5a831a52d36372ce975897e6ee84f52c53f8048adf306d", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/job/feedback/mod.rs", + "byte_length": 32, + "sha256": "f72b49a09f84d980791360980c31534c1058bd90ba181ba7e97d871663998c0d", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/job/mod.rs", + "byte_length": 112, + "sha256": "6428863fbbd2e08e18e6308cbbf1bb73ab92e4f2772933bc0011f91f3d5ef2a4", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/job/request/decode.rs", + "byte_length": 2778, + "sha256": "d8cc34984390a8e0a29db46b3b94baabeb5a89dd1ae1fb8a425fbf7f5f55a148", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/job/request/encode.rs", + "byte_length": 2408, + "sha256": "11b77bbb3eed9b36bdc7b6f9055ff2228864dc364fda78267f75d0411c9bb7b2", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/job/request/mod.rs", + "byte_length": 32, + "sha256": "f72b49a09f84d980791360980c31534c1058bd90ba181ba7e97d871663998c0d", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/job/result/decode.rs", + "byte_length": 3083, + "sha256": "c38d16fa824c3320c7ebd5e0de6eb0eebab4c8265fc28911b8e60337eba04938", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/job/result/encode.rs", + "byte_length": 2366, + "sha256": "37b71cdebf674c8ca8afd0956361054d79e981d07e0285b5987f48554a9f4f8b", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/job/result/mod.rs", + "byte_length": 32, + "sha256": "f72b49a09f84d980791360980c31534c1058bd90ba181ba7e97d871663998c0d", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/job/traits.rs", + "byte_length": 5037, + "sha256": "462db579a3ca9df48982c152c27a0770d5c5e5f4fb8b2b875b5a60bc03fa6588", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/job/util.rs", + "byte_length": 7101, + "sha256": "5f8a3d59e5fad7bcfe9694f028e8803e6ca523adc909e7f487ec7e6acccd77ab", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/knowledge/decode.rs", + "byte_length": 24681, + "sha256": "eecdf6fc09a4d9b4a5aade9f05d8d9b1028707802df7e5453dad76d4feb3ba6d", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/knowledge/encode.rs", + "byte_length": 14873, + "sha256": "19bb647940c47d0e8fcf468af98b3ac7c101ee2b8b8db4b7f66dd900a80c85ee", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/knowledge/mod.rs", + "byte_length": 1197, + "sha256": "d18ed0e257f8e1e31cebd155f1fffb469a1bb6922c11e4088c762c38fee9e494", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/knowledge/verification.rs", + "byte_length": 7212, + "sha256": "7c9b078814dd1694876fb1231c6bdd6c57fbb3e8aeb7c6d26b32571255446176", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/lib.rs", + "byte_length": 2556, + "sha256": "f844cd20eefad594ede4fff0122b6d72081a68c15afefba7cc926b23820fcdd8", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/list/decode.rs", + "byte_length": 3787, + "sha256": "c1e10e7a9fee7f2cf9a2c8ae1f8f49d1d05a4e6891c775fff0e08799fc8f9b31", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/list/encode.rs", + "byte_length": 3176, + "sha256": "eaac989d2983d1be6b528a91132489159d39adce0c273cc127e245fdbcd85da0", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/list/mod.rs", + "byte_length": 1913, + "sha256": "85425ed97d83ce82e7f845308475359a7ffcf33a54f05e0b34636c6c9833df76", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/list_set/decode.rs", + "byte_length": 4090, + "sha256": "3f495165a5b40274b732a4238784320bed46d054451d4afbb6e8eb3f4dcb197b", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/list_set/encode.rs", + "byte_length": 2772, + "sha256": "5d78f890610c42dc8c00bfa3bf8d4bcaeb4d89b9443b5f93c5045868f2f07069", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/list_set/mod.rs", + "byte_length": 7285, + "sha256": "65cddeae6751d636c34a445faf3d85537ccaa8836ebe7fa7a4f6278502492f22", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/manifest.rs", + "byte_length": 21199, + "sha256": "f8db83b093d791cacdd406712e1b9d2b914d88ee8295f43fa44b0ed06849d085", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/manifest/registry_v7.rs", + "byte_length": 38620, + "sha256": "93c3150f2f74264340264f68b2df328ed86fa7b5f39529bc8abac428137cfe16", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/message/decode.rs", + "byte_length": 1935, + "sha256": "30d0010373463c20c7e8687047a05e30a875712a7762b4ac26bcbab7af913083", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/message/encode.rs", + "byte_length": 1146, + "sha256": "704c24edd8b9480e2f83d5ce6eec231314c92cbdc809c137ad57e10831f4e4bb", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/message/mod.rs", + "byte_length": 78, + "sha256": "da18b75c116d8885b5f2f5218723d45af00dc8fed82d71564e1e664e8023fe70", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/message/tags.rs", + "byte_length": 11847, + "sha256": "831ce6c747c30919386511e8397f0f2c9d145f03ad05a7e7a2a01e74ca4f0db6", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/message_file/decode.rs", + "byte_length": 5654, + "sha256": "5cb3ea388d206cfcc7ff90a23f6f24d4767f8c2ce8f5f5276421a8b4ea74ec37", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/message_file/encode.rs", + "byte_length": 3580, + "sha256": "330d6efefd553c6774a47d5e28b74c33ca940876b21d6cd80c22901f0f83dab9", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/message_file/mod.rs", + "byte_length": 57, + "sha256": "5c57058e78fa54008ba63a97ba12aee00db28753fa39bdff3cb7347e3447dabc", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/operational_listing/decode.rs", + "byte_length": 33577, + "sha256": "91acebfd9760417a9a11f7f8b46e7412c379792f7cbf96f343483b485db29415", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/operational_listing/encode.rs", + "byte_length": 5383, + "sha256": "d6287fe1167b4fd6b7119a32d2b9f1daa77fd4217f339d23d7a20d996c654c68", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/operational_listing/mod.rs", + "byte_length": 46, + "sha256": "0cbdae9059f46d9158b7b4ecc6814718fee407b6702903d16deff0da48970766", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/operational_listing/tags.rs", + "byte_length": 51990, + "sha256": "3a5d625fd652da1039fa7d16a9699ceae98ba1940bf1e081bdbc53f77f50026d", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/order/decode.rs", + "byte_length": 44114, + "sha256": "40a30b1e15b06e93ede14082b0a048070624bc0707fab689081903d462262978", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/order/encode.rs", + "byte_length": 14566, + "sha256": "13a6b946188d418fdb4dbc67fda31c413ff176efd2375e1112c3efdd31528e41", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/order/mod.rs", + "byte_length": 677, + "sha256": "1afecc3a177c814dc2b02499ec4e6480aaa7814a8cab350e102db50023c3565a", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/order/tags.rs", + "byte_length": 23506, + "sha256": "ee115a43f984222777926255c491cbad438e1662b094906c974658bffa1237bc", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/parsed.rs", + "byte_length": 5486, + "sha256": "a91ad5954c12662be047db90fb9baaf9772690f8515a55235b4e3fa191f9aea7", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/plot/decode.rs", + "byte_length": 4572, + "sha256": "5a307aeb02f7201079cfe632d8d2a1087ca6f7754849cd1693a572a79a69e84d", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/plot/encode.rs", + "byte_length": 3476, + "sha256": "1c4ab8b964f1de524c0666fee14ae818be298ee69b657d460f39fbff3c5e707d", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/plot/mod.rs", + "byte_length": 8421, + "sha256": "d18b8cbed3f921f436bcc63dcac2d830a05ac66c75c29440a8195b5cbff323b8", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/post/admission.rs", + "byte_length": 5428, + "sha256": "e64ce9b93280ab259ee3738df2c880d3c50b4682f1bf903fc19c86f5f0555d32", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/post/authored.rs", + "byte_length": 2104, + "sha256": "85f1c9c0af9f1f46de17f0a9fc74c915d4ad948b39df32787f16b36d4abe19b8", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/post/decode.rs", + "byte_length": 8987, + "sha256": "83a0e3a237012a04a932e6d9c205906ea72ff35669928e03bb7107facfe63c17", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/post/inbound.rs", + "byte_length": 86, + "sha256": "9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/post/inbound/registry_v7.rs", + "byte_length": 16016, + "sha256": "bc0bb839c28f3588f05544c846c987528ea3934c75305fef70af8e027a97cc58", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/post/inbound/registry_v7/tests.rs", + "byte_length": 9031, + "sha256": "77d6c75e25551df1ebf33a6a6d240038d52270186aa81dde9618f31c87c32cf9", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/post/mod.rs", + "byte_length": 70, + "sha256": "12e3b8d15be0efa6074ee506e0562f9dbdc2fe761122bbaf95c2624939015b7e", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/profile/admission.rs", + "byte_length": 4091, + "sha256": "a4d46f055b99c7d607e4393d031cb8f0975ab9a4e105f6b63bfb56ee48f2475f", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/profile/authored.rs", + "byte_length": 6279, + "sha256": "6be8823575f718e421935eef5b6822e6ee0232876ce669c1bf57b13392b51001", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/profile/decode.rs", + "byte_length": 4065, + "sha256": "09bacfa0b677291eeb1e945997a80deaa9b06951168f029c2c5c297c01efd2c2", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/profile/inbound.rs", + "byte_length": 86, + "sha256": "9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/profile/inbound/registry_v7.rs", + "byte_length": 10394, + "sha256": "c37c225ea8153beb098a39c4a56203aa03382af12646f40cd3f8628663700702", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/profile/inbound/registry_v7/tests.rs", + "byte_length": 279, + "sha256": "6191b1bdc576ed91be1998deab3d20fe55a49adfd149d2f80ca9038174153d7a", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/profile/mod.rs", + "byte_length": 521, + "sha256": "5d1a3b92f34dcd5a4ce340be97fcdd5244687203940ac6056353b25f7c5f4122", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/reaction/decode.rs", + "byte_length": 4825, + "sha256": "f719e16e317bdb7c73bc0950575a29408f09610ad2cf7b5c30451b3229e7c8e7", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/reaction/encode.rs", + "byte_length": 4294, + "sha256": "31a5c8f090753e5b512f3b8931665ed86d5716c236c9d09881b108178037403c", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/reaction/mod.rs", + "byte_length": 32, + "sha256": "f72b49a09f84d980791360980c31534c1058bd90ba181ba7e97d871663998c0d", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/relay_auth/decode.rs", + "byte_length": 1799, + "sha256": "36114252d1096b839c60851ea8b678a39ef931a1c18c811528f343293e1b3387", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/relay_auth/encode.rs", + "byte_length": 1288, + "sha256": "add7ae05d38c2c495be2c0dcddaf5f9ebef125cf3c7bbc5f6c9229311644cf2e", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/relay_auth/mod.rs", + "byte_length": 5736, + "sha256": "71a0834e9e312dc69319578a1aa69d5f79a10e3dcb219b37dea57a0839510304", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/relay_document/decode.rs", + "byte_length": 309, + "sha256": "90302e941a2ae5d65965e6dc0ce1ef1e416fc4257a45c067da94cccebb9909aa", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/relay_document/encode.rs", + "byte_length": 338, + "sha256": "884eeceb1bd605c970b5bc17ef662ec5a2c463c2dbdc3102f6888e768924f2cb", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/relay_document/mod.rs", + "byte_length": 32, + "sha256": "f72b49a09f84d980791360980c31534c1058bd90ba181ba7e97d871663998c0d", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/reply/admission.rs", + "byte_length": 3842, + "sha256": "b2723a29d5f10389d4f746dc9d242a96bca26071b34c41429af32e68fa2c62a9", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/reply/authored.rs", + "byte_length": 2521, + "sha256": "4141356b803e554d54a9667bd3f70c62fc8d6f743768a7a103b177bb7847ea88", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/reply/inbound.rs", + "byte_length": 86, + "sha256": "9994cbca9dcc7450ad133b6b927e7c06bee08ef813652452cc1e56a284c1be10", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/reply/inbound/registry_v7.rs", + "byte_length": 26837, + "sha256": "5b929d040f751520e15d52ccf44d7622a3eee44f136d958411515be67742bab3", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/reply/inbound/registry_v7/tests.rs", + "byte_length": 13030, + "sha256": "80a98b1f70193e2e4d9cd25bd8c31848914f55b1e47fb1dc222ae86ccb67aa10", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/reply/mod.rs", + "byte_length": 54, + "sha256": "986f59844194fbaac78618c6131940691d49dc888702366b3520c901a08990e0", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/report/decode.rs", + "byte_length": 6021, + "sha256": "bb7ae904636855252584dca0e2016a5aaef96ba1dbd596ff4b9601835c66d68b", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/report/encode.rs", + "byte_length": 5288, + "sha256": "35df56a61ee96e7cddaf73d7a0ef236263502be01229f0546f90f93fa609ca73", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/report/mod.rs", + "byte_length": 32, + "sha256": "f72b49a09f84d980791360980c31534c1058bd90ba181ba7e97d871663998c0d", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/repost/decode.rs", + "byte_length": 5627, + "sha256": "45635907279f3d70247a8dd3a753524da4d4680168f406740fd4a64efd48422e", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/repost/encode.rs", + "byte_length": 5973, + "sha256": "7765baebbe0f34cd6a763f4a328d3f1d794870f1141c9910a130fe14b0ab39b3", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/repost/mod.rs", + "byte_length": 32, + "sha256": "f72b49a09f84d980791360980c31534c1058bd90ba181ba7e97d871663998c0d", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/resource_area/decode.rs", + "byte_length": 2573, + "sha256": "59e68071790bfe9775355225336287b04eabd926d6c0b391e5f9711fd05e1cbc", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/resource_area/encode.rs", + "byte_length": 3099, + "sha256": "fa92ecbd5d5bf740c336842e02a66984e821931fe9e1fb890b43b966429227d7", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/resource_area/list_sets.rs", + "byte_length": 10445, + "sha256": "f3b9f9aefe3436b854098a53ea3aa13c5e08cf01ea56a2bca1c4a1b207e30e29", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/resource_area/mod.rs", + "byte_length": 8608, + "sha256": "73e2e5196e2eed084be2cbc0e3c34dc25d6621c74af15bc5673087f665e0dd18", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/resource_cap/decode.rs", + "byte_length": 2583, + "sha256": "883b7df150311ad3e500500e93398bb3bb337934bb1a6e95fbd5e3af554336bf", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/resource_cap/encode.rs", + "byte_length": 5463, + "sha256": "1e039a919f379cc90ade5c220904f140213b8161bba9234355705160a0b5f2c0", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/resource_cap/mod.rs", + "byte_length": 3535, + "sha256": "971797348274938b6ddf9674e85d9b680de76e5fdea74689c16df5da49d4c10d", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/seal/decode.rs", + "byte_length": 1716, + "sha256": "eb0efa8796c0e32bb0709f3f29f54c9464d1aa3552dc3c49cc4571fedffb316d", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/seal/encode.rs", + "byte_length": 1029, + "sha256": "1789c7e442002270874b20e8c1b1edde66eae9cf5e4e9ac85a1ffb5a79b199dc", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/seal/mod.rs", + "byte_length": 57, + "sha256": "5c57058e78fa54008ba63a97ba12aee00db28753fa39bdff3cb7347e3447dabc", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/social_helpers.rs", + "byte_length": 11112, + "sha256": "94e2b757165c393c75a8005c71227019ab6fe72084aae4293a4c1fae7c424403", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/tag_builders.rs", + "byte_length": 9603, + "sha256": "f51b0bcb52735223c077949d29b7b5d7f8e97dbf12f8ea0a7fa1cd36420f46ee", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/test_fixtures.rs", + "byte_length": 4697, + "sha256": "f6e82c0cf780bcd12e559e3900c7eebd27287f21c9804b73ed180e4b45286146", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/trade/mod.rs", + "byte_length": 22362, + "sha256": "59605e4dac95bd211a15e1bbecb2a5c23adcb4fe71f1535d07b27aceca5a0604", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/verification.rs", + "byte_length": 253, + "sha256": "b49a32df605035c87f295c0a151140d43d2e588c5e11b05183e5fc92993dae0e", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/verification/v1.rs", + "byte_length": 8739, + "sha256": "79eff26d9ea7207367c6667b2484c992a3a400a75b654012b4708e9f716b7e7e", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/verification/v1/tests.rs", + "byte_length": 3634, + "sha256": "721cdee3c2b3c9fe5c74542460d4a44a6f02e9339391588ca2cc8573292a5cb4", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/wire.rs", + "byte_length": 644, + "sha256": "8debb7b142d11e841a2bd03434002d03206914ce7d45247145dfaf0ac72dfbe8", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/wire/publication.rs", + "byte_length": 59629, + "sha256": "bd8130f4e1b0d3e20a03014449001c72eecc4fb97051c8877287e6bbbb309319", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/event_codec/src/wire/publication/allowlist.rs", + "byte_length": 21115, + "sha256": "d3bb5522a68b3f68d369c380403fec33c247e689288515446a8b8f5bd6b1ba5e", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "publication_allowlist_vector_executor", + "path": "crates/event_codec/tests/publication_allowlist.rs", + "byte_length": 7562, + "sha256": "342d3d3d1100cb5d31aca94b0540b1e03761b034de23475bdb5142baabeed8fd", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "publication_vector_executor", + "path": "crates/event_codec/tests/publication_artifact.rs", + "byte_length": 34582, + "sha256": "7a31169eac4217a38cb3ef25eb9213f2f89e11fb17e76ceaf7449b34225e98af", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "event_store_manifest_authority", + "path": "crates/event_store/Cargo.toml", + "byte_length": 1529, + "sha256": "4bddb3462a7543c9a7981ead5cf1027988fc381457432f4b04b0e9c43f6d51ca", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "transport_manifest_authority", + "path": "crates/transport/Cargo.toml", + "byte_length": 706, + "sha256": "98856ba5a00bf7183a1f2d0120cccb50ab68cbbc3091ba15d4c8f7d0052d364b", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/transport/src/delivery.rs", + "byte_length": 20085, + "sha256": "d80bba7e20c3c80ebffef23314220038f16f508c64ba4836e3d04280e199f6ff", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/transport/src/error.rs", + "byte_length": 5352, + "sha256": "a26488936a6d844622975d9fd8a000f7cd3d4d4967f8689bd0fe9458cb6c27a9", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/transport/src/kind.rs", + "byte_length": 2943, + "sha256": "9cc8a4b7c95442fd9d2d902a2d47f98c8752355bcb17f7c4b9cdbf50f71b54b7", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/transport/src/lib.rs", + "byte_length": 1916, + "sha256": "e9b9bd50dc5ee19f92ab75b5f277c10e9b330d36b9d68fbb7db57d27ddb76621", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/transport/src/message.rs", + "byte_length": 297, + "sha256": "722a0b50aa90508ebdacbc87751dc296c38aa925920a9aa9ce8e88d0f6d73fd8", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/transport/src/payload.rs", + "byte_length": 9895, + "sha256": "f2fd757050471b2055b2b1cf71f311bd2c729e009ee802996a33a92d67eaad2d", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/transport/src/reticulum.rs", + "byte_length": 8193, + "sha256": "4c2846f0081cd2f7560805a9b8f79b0af4417a575b7d6d6e44aa86975a124d61", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/transport/src/status.rs", + "byte_length": 11330, + "sha256": "cd99b96c1fd370d0e88c1cbb159e952e16a747a72d5cc967b71784123dc1947f", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/transport/src/target.rs", + "byte_length": 23937, + "sha256": "fa788cba1f5bac28bd0084ebaf5f0daea5d037f5de6ef74836fd46a6054ea3d0", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "public_production_source", + "path": "crates/transport/src/transport.rs", + "byte_length": 2060, + "sha256": "4d2f639f7214bc06c8cc7f7483513a706221f8e5f2f3f03f79f24892e8acb729", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "rust_toolchain_authority", + "path": "rust-toolchain.toml", + "byte_length": 132, + "sha256": "c33aa38292bab6513bf79ed2f69c1525b736dd738b15ca78af713b70b29265c9", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "xtask_manifest_authority", + "path": "tools/xtask/Cargo.toml", + "byte_length": 1173, + "sha256": "b915e0289bf7390d3c4194aaed1e748cf5e591426e0443c310775ddc7d7f63a5", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "contract_command_authority", + "path": "tools/xtask/src/contract.rs", + "byte_length": 481424, + "sha256": "e1f5867b5861b5f2a7dd69e22d41c8abc8e484458d0a19b8659c81501897d2cd", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "superseded_nip09_contract_governance", + "path": "tools/xtask/src/contract/nip09_reconciliation.rs", + "byte_length": 852432, + "sha256": "971d75198770265b17e17df23ba17a6096ad687d1cf2ac0f662a2c190ab39f94", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "publication_allowlist_contract_governance", + "path": "tools/xtask/src/contract/phase1_publication_allowlist.rs", + "byte_length": 76855, + "sha256": "9d6459123f78d3de282fafa5b12daf253c79c378da2b225ae780a69d56784a42", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "publication_contract_governance", + "path": "tools/xtask/src/contract/phase1_publication_artifact.rs", + "byte_length": 75139, + "sha256": "3f0df4f48ee2d2dcd6943c591bba746d2ebb4c179b1825563e15d8b33302cddf", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "superseded_raw_rebuild_contract_governance", + "path": "tools/xtask/src/contract/raw_source_rebuild.rs", + "byte_length": 297547, + "sha256": "e8cf84ba8f27432d0ba7aefa01fc61f9e37810d48f407fa2b9c7c969e5c76724", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "contract_dispatch_authority", + "path": "tools/xtask/src/main.rs", + "byte_length": 17100, + "sha256": "cce879d7d351fd1d359a0368cf84edb31412e3d17d730b3ddc19f56ff23d3bea", + "hash_algorithm": "sha256_bytes_v1" + } + ], + "result_vector": { + "canonical_path": "contracts/conformance/vectors/publication/phase1_allowlist.v1.json", + "mirror_path": "crates/event_codec/tests/fixtures/phase1_publication_allowlist.v1.json", + "byte_length": 49975, + "sha256": "2867ee401db8cfad3a77869847c57567e869623f55d3d6c9e98a7fa0a643c3d6", + "hash_algorithm": "sha256_bytes_v1", + "executor_path": "crates/event_codec/tests/publication_allowlist.rs", + "executor_test": "publication_allowlist_conformance_vector_executes_every_case", + "valid_case_ids": [ + "sealed_profile_is_allowed", + "sealed_update_is_allowed", + "sealed_photo_update_is_allowed", + "sealed_ask_is_allowed", + "sealed_date_event_is_allowed", + "sealed_time_event_is_allowed", + "sealed_food_availability_is_allowed", + "canonical_profile_is_allowed", + "canonical_update_is_allowed", + "canonical_photo_update_is_allowed", + "canonical_ask_is_allowed", + "canonical_date_event_is_allowed", + "canonical_time_event_is_allowed", + "canonical_food_availability_is_allowed" + ], + "invalid_case_ids": [ + "update_claim_with_ask_marker_is_rejected", + "photo_claim_with_ask_marker_is_rejected", + "ask_claim_without_marker_is_rejected", + "update_claim_with_photo_profile_is_rejected", + "typed_date_event_with_generic_shape_is_rejected", + "typed_time_event_with_generic_shape_is_rejected", + "operational_listing_profile_is_rejected", + "generic_nip99_profile_is_rejected", + "mixed_food_and_operational_markers_are_rejected", + "unsealed_profile_is_rejected", + "generic_root_note_is_rejected", + "reply_is_rejected", + "comment_is_rejected", + "deletion_request_is_rejected", + "raw_date_event_is_rejected", + "raw_time_event_is_rejected", + "calendar_collection_is_rejected", + "calendar_rsvp_is_rejected", + "bud11_authorization_is_rejected", + "ephemeral_event_is_rejected", + "trade_event_is_rejected", + "order_event_is_rejected", + "route_delivery_family_is_rejected", + "group_event_is_rejected", + "operations_event_is_rejected" + ] + }, + "release": { + "record_path": "contracts/releases/1.0.0-alpha.1.toml", + "change_id": "phase1-publication-allowlist", + "changelog_path": "CHANGELOG.md", + "changelog_marker": "<!-- release-change: phase1-publication-allowlist -->" + } +} diff --git a/crates/event_codec/contracts/phase1_publication_allowlist_v1.manifest.schema.json b/crates/event_codec/contracts/phase1_publication_allowlist_v1.manifest.schema.json @@ -0,0 +1,352 @@ +{ + "$defs": { + "file": { + "additionalProperties": false, + "properties": { + "byte_length": { + "minimum": 1, + "type": "integer" + }, + "hash_algorithm": { + "const": "sha256_bytes_v1" + }, + "path": { + "minLength": 1, + "type": "string" + }, + "sha256": { + "pattern": "^[0-9a-f]{64}$", + "type": "string" + } + }, + "required": [ + "path", + "byte_length", + "sha256", + "hash_algorithm" + ], + "type": "object" + }, + "leaf": { + "additionalProperties": false, + "properties": { + "authored_operation_id": { + "minLength": 1, + "type": "string" + }, + "event_contract_id": { + "minLength": 1, + "type": "string" + }, + "kind": { + "minimum": 0, + "type": "integer" + }, + "leaf": { + "minLength": 1, + "type": "string" + }, + "publication_role": { + "enum": [ + "supporting", + "root" + ] + } + }, + "required": [ + "leaf", + "kind", + "publication_role", + "authored_operation_id", + "event_contract_id" + ], + "type": "object" + }, + "source": { + "additionalProperties": false, + "properties": { + "byte_length": { + "minimum": 1, + "type": "integer" + }, + "hash_algorithm": { + "const": "sha256_bytes_v1" + }, + "path": { + "minLength": 1, + "type": "string" + }, + "role": { + "minLength": 1, + "type": "string" + }, + "sha256": { + "pattern": "^[0-9a-f]{64}$", + "type": "string" + } + }, + "required": [ + "role", + "path", + "byte_length", + "sha256", + "hash_algorithm" + ], + "type": "object" + } + }, + "$id": "https://radroots.org/contracts/event-codec/phase1-publication-allowlist-v1.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "allowlist": { + "additionalProperties": false, + "properties": { + "allowed_leaves": { + "items": { + "$ref": "#/$defs/leaf" + }, + "maxItems": 7, + "minItems": 7, + "type": "array" + }, + "canonical_json_operation_id": { + "const": "publication_allowlist.allow_canonical_json" + }, + "classified_listing_policy": { + "const": "raw_marker_partition_before_focused_food_profile_validation_v1" + }, + "contract_id": { + "const": "radroots.phase1.publication_allowlist.v1" + }, + "denied_families": { + "items": { + "minLength": 1, + "type": "string" + }, + "maxItems": 19, + "minItems": 19, + "type": "array" + }, + "event_policy": { + "const": "strict_typed_nip52_date_or_time_artifact_only_v1" + }, + "excluded_capabilities": { + "items": { + "minLength": 1, + "type": "string" + }, + "maxItems": 7, + "minItems": 7, + "type": "array" + }, + "granted_capability": { + "const": "phase1_durable_publication_lane_entry_only_v1" + }, + "input_type": { + "const": "RadrootsPhase1PublicationArtifact" + }, + "kind_one_precedence": { + "const": [ + "ask", + "photo_update", + "update" + ] + }, + "operation_id": { + "const": "publication_allowlist.allow_artifact" + }, + "output_type": { + "const": "RadrootsPhase1AllowlistedPublicationArtifact" + }, + "version": { + "const": 1 + } + }, + "required": [ + "version", + "contract_id", + "operation_id", + "canonical_json_operation_id", + "input_type", + "output_type", + "allowed_leaves", + "kind_one_precedence", + "event_policy", + "classified_listing_policy", + "denied_families", + "granted_capability", + "excluded_capabilities" + ], + "type": "object" + }, + "authority_id": { + "const": "phase1_publication_allowlist_v1" + }, + "contract_id": { + "const": "radroots_event_codec.phase1_publication_allowlist_v1" + }, + "event_contract_registry": { + "additionalProperties": false, + "properties": { + "evolution": { + "const": "immutable_registry_v7_plus_additive_phase1_publication_allowlist_v1" + }, + "inventory": { + "$ref": "#/$defs/file" + }, + "sidecar": { + "$ref": "#/$defs/file" + }, + "version": { + "const": 7 + } + }, + "required": [ + "version", + "inventory", + "sidecar", + "evolution" + ], + "type": "object" + }, + "manifest_schema": { + "$ref": "#/$defs/file" + }, + "predecessor": { + "additionalProperties": false, + "properties": { + "contract_id": { + "const": "radroots_event_codec.phase1_publication_artifact_v1" + }, + "immutable_artifacts": { + "items": { + "$ref": "#/$defs/file" + }, + "maxItems": 6, + "minItems": 6, + "type": "array" + } + }, + "required": [ + "contract_id", + "immutable_artifacts" + ], + "type": "object" + }, + "predecessor_source_supersessions": { + "items": { + "minLength": 1, + "type": "string" + }, + "maxItems": 8, + "minItems": 8, + "type": "array" + }, + "release": { + "additionalProperties": false, + "properties": { + "change_id": { + "const": "phase1-publication-allowlist" + }, + "changelog_marker": { + "const": "<!-- release-change: phase1-publication-allowlist -->" + }, + "changelog_path": { + "const": "CHANGELOG.md" + }, + "record_path": { + "const": "contracts/releases/1.0.0-alpha.1.toml" + } + }, + "required": [ + "record_path", + "change_id", + "changelog_path", + "changelog_marker" + ], + "type": "object" + }, + "result_vector": { + "additionalProperties": false, + "properties": { + "byte_length": { + "minimum": 1, + "type": "integer" + }, + "canonical_path": { + "const": "contracts/conformance/vectors/publication/phase1_allowlist.v1.json" + }, + "executor_path": { + "const": "crates/event_codec/tests/publication_allowlist.rs" + }, + "executor_test": { + "const": "publication_allowlist_conformance_vector_executes_every_case" + }, + "hash_algorithm": { + "const": "sha256_bytes_v1" + }, + "invalid_case_ids": { + "items": { + "type": "string" + }, + "maxItems": 25, + "minItems": 25, + "type": "array" + }, + "mirror_path": { + "const": "crates/event_codec/tests/fixtures/phase1_publication_allowlist.v1.json" + }, + "sha256": { + "pattern": "^[0-9a-f]{64}$", + "type": "string" + }, + "valid_case_ids": { + "items": { + "type": "string" + }, + "maxItems": 14, + "minItems": 14, + "type": "array" + } + }, + "required": [ + "canonical_path", + "mirror_path", + "byte_length", + "sha256", + "hash_algorithm", + "executor_path", + "executor_test", + "valid_case_ids", + "invalid_case_ids" + ], + "type": "object" + }, + "schema_version": { + "const": 1 + }, + "source_files": { + "items": { + "$ref": "#/$defs/source" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "schema_version", + "contract_id", + "authority_id", + "manifest_schema", + "predecessor", + "event_contract_registry", + "allowlist", + "predecessor_source_supersessions", + "source_files", + "result_vector", + "release" + ], + "title": "Radroots Phase 1 Publication Allowlist Contract", + "type": "object" +} diff --git a/crates/event_codec/contracts/phase1_publication_allowlist_v1.manifest.sha256 b/crates/event_codec/contracts/phase1_publication_allowlist_v1.manifest.sha256 @@ -0,0 +1 @@ +5ba2e7d6cdd616b423d3bdbcf2c6d2644d4f2568b1eb13cbaacf101a076f485c diff --git a/crates/event_codec/src/wire/publication.rs b/crates/event_codec/src/wire/publication.rs @@ -11,6 +11,8 @@ //! signer, or control the host/database. NIP-01 id and signature verification //! remain the cryptographic authenticity boundary. +pub mod allowlist; + #[cfg(not(feature = "std"))] use alloc::{ format, diff --git a/crates/event_codec/src/wire/publication/allowlist.rs b/crates/event_codec/src/wire/publication/allowlist.rs @@ -0,0 +1,544 @@ +//! Strict publication allowlist for sealed Phase 1 artifacts. +//! +//! This authority is additive to event-contract registry v7. It answers only +//! whether a sealed authored artifact may enter the Phase 1 durable +//! publication lane. It does not broaden inbound admission, sign an event, +//! prove media readiness, publish to a relay, or grant product entitlement. + +use core::fmt; + +use radroots_event::{ + RadrootsEventTags, + classified_listing::RadrootsClassifiedListingPartition, + kinds::{ + KIND_CALENDAR_DATE_EVENT, KIND_CALENDAR_TIME_EVENT, KIND_CLASSIFIED_LISTING, KIND_POST, + KIND_PROFILE, + }, +}; + +use crate::{ + food_availability::inbound::{ + RadrootsFoodAvailabilityProjectionOutcome, + registry_v7::project_inbound_food_availability_parts, + }, + post::inbound::{RadrootsPostClassification, registry_v7::project_inbound_post_parts}, +}; + +use super::{ + RadrootsPhase1PublicationArtifact, RadrootsPhase1PublicationArtifactError, + RadrootsPhase1PublicationEventVariant, RadrootsPhase1PublicationSemanticVariant, + validate_calendar_date, validate_calendar_time, validate_food_availability, + validate_phase1_publication_artifact, validate_profile, +}; + +pub const RADROOTS_PHASE1_PUBLICATION_ALLOWLIST_VERSION: u32 = 1; +pub const RADROOTS_PHASE1_PUBLICATION_ALLOWLIST_CONTRACT_ID: &str = + "radroots.phase1.publication_allowlist.v1"; +pub const RADROOTS_PHASE1_PUBLICATION_ALLOWLIST_REGISTRY_VERSION: u32 = 7; + +/// The exact seven authored leaves accepted by the Phase 1 publication lane. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum RadrootsPhase1PublicationLeaf { + Profile, + Update, + PhotoUpdate, + Ask, + EventDate, + EventTime, + FoodAvailability, +} + +impl RadrootsPhase1PublicationLeaf { + pub const fn as_str(self) -> &'static str { + match self { + Self::Profile => "profile", + Self::Update => "update", + Self::PhotoUpdate => "photo_update", + Self::Ask => "ask", + Self::EventDate => "event_date", + Self::EventTime => "event_time", + Self::FoodAvailability => "food_availability", + } + } + + pub const fn kind(self) -> u32 { + self.semantic_variant().kind() + } + + pub const fn authored_operation_id(self) -> &'static str { + self.semantic_variant().authored_operation_id() + } + + pub const fn event_contract_id(self) -> &'static str { + self.semantic_variant().event_contract_id() + } + + const fn from_semantic_variant(variant: RadrootsPhase1PublicationSemanticVariant) -> Self { + match variant { + RadrootsPhase1PublicationSemanticVariant::Profile => Self::Profile, + RadrootsPhase1PublicationSemanticVariant::Update => Self::Update, + RadrootsPhase1PublicationSemanticVariant::PhotoUpdate => Self::PhotoUpdate, + RadrootsPhase1PublicationSemanticVariant::Ask => Self::Ask, + RadrootsPhase1PublicationSemanticVariant::Event( + RadrootsPhase1PublicationEventVariant::Date, + ) => Self::EventDate, + RadrootsPhase1PublicationSemanticVariant::Event( + RadrootsPhase1PublicationEventVariant::Time, + ) => Self::EventTime, + RadrootsPhase1PublicationSemanticVariant::FoodAvailability => Self::FoodAvailability, + } + } + + const fn semantic_variant(self) -> RadrootsPhase1PublicationSemanticVariant { + match self { + Self::Profile => RadrootsPhase1PublicationSemanticVariant::Profile, + Self::Update => RadrootsPhase1PublicationSemanticVariant::Update, + Self::PhotoUpdate => RadrootsPhase1PublicationSemanticVariant::PhotoUpdate, + Self::Ask => RadrootsPhase1PublicationSemanticVariant::Ask, + Self::EventDate => RadrootsPhase1PublicationSemanticVariant::Event( + RadrootsPhase1PublicationEventVariant::Date, + ), + Self::EventTime => RadrootsPhase1PublicationSemanticVariant::Event( + RadrootsPhase1PublicationEventVariant::Time, + ), + Self::FoodAvailability => RadrootsPhase1PublicationSemanticVariant::FoodAvailability, + } + } +} + +impl fmt::Display for RadrootsPhase1PublicationLeaf { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(self.as_str()) + } +} + +/// A sealed artifact that passed the additive Phase 1 publication allowlist. +/// +/// Private fields prevent callers from pairing a leaf with a different +/// artifact. Construction is available only through +/// [`allow_phase1_publication_artifact`] and its strict canonical-reload adapter +/// [`allow_phase1_publication_canonical_json`]. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsPhase1AllowlistedPublicationArtifact { + artifact: RadrootsPhase1PublicationArtifact, + leaf: RadrootsPhase1PublicationLeaf, +} + +impl RadrootsPhase1AllowlistedPublicationArtifact { + pub const fn leaf(&self) -> RadrootsPhase1PublicationLeaf { + self.leaf + } + + pub fn artifact(&self) -> &RadrootsPhase1PublicationArtifact { + &self.artifact + } + + pub fn into_artifact(self) -> RadrootsPhase1PublicationArtifact { + self.artifact + } +} + +/// Revalidates and reprojects a sealed artifact before granting Phase 1 +/// publication-lane authority. +/// +/// Kind `1` is classified with Ask-before-PhotoUpdate-before-Update +/// precedence. Calendar kinds must retain their strict date/time authored +/// profile, and kind `30402` is marker-partitioned before focused Food profile +/// validation. Every other kind and product family fails closed. +pub fn allow_phase1_publication_artifact( + artifact: RadrootsPhase1PublicationArtifact, +) -> Result<RadrootsPhase1AllowlistedPublicationArtifact, RadrootsPhase1PublicationAllowlistError> { + validate_phase1_publication_artifact(&artifact) + .map_err(RadrootsPhase1PublicationAllowlistError::ArtifactInvalid)?; + + let claimed = RadrootsPhase1PublicationLeaf::from_semantic_variant(artifact.semantic_variant()); + let projected = reproject_publication_leaf(&artifact)?; + if claimed != projected { + return Err( + RadrootsPhase1PublicationAllowlistError::SemanticVariantMismatch { claimed, projected }, + ); + } + + Ok(RadrootsPhase1AllowlistedPublicationArtifact { + artifact, + leaf: projected, + }) +} + +/// Reloads exact canonical artifact bytes before applying the Phase 1 +/// publication allowlist. +/// +/// This is the durable-reload boundary for persisted `09a0a` artifacts. Raw +/// NIP-01 event JSON and malformed or semantically invalid artifact candidates +/// fail in the predecessor parser and are returned as +/// [`RadrootsPhase1PublicationAllowlistError::ArtifactInvalid`]. +pub fn allow_phase1_publication_canonical_json( + bytes: &[u8], +) -> Result<RadrootsPhase1AllowlistedPublicationArtifact, RadrootsPhase1PublicationAllowlistError> { + let artifact = RadrootsPhase1PublicationArtifact::from_canonical_json(bytes) + .map_err(RadrootsPhase1PublicationAllowlistError::ArtifactInvalid)?; + allow_phase1_publication_artifact(artifact) +} + +fn reproject_publication_leaf( + artifact: &RadrootsPhase1PublicationArtifact, +) -> Result<RadrootsPhase1PublicationLeaf, RadrootsPhase1PublicationAllowlistError> { + let draft = artifact.draft(); + match draft.kind() { + KIND_PROFILE => { + validate_profile(draft, artifact.media_references()) + .map_err(|_| RadrootsPhase1PublicationAllowlistError::ProfileInvalid)?; + Ok(RadrootsPhase1PublicationLeaf::Profile) + } + KIND_POST => { + let projection = + project_inbound_post_parts(draft.kind(), draft.tags(), draft.content()).map_err( + |error| RadrootsPhase1PublicationAllowlistError::PostInvalid { + source_code: error.code(), + }, + )?; + if !projection.diagnostics().is_empty() { + return Err(RadrootsPhase1PublicationAllowlistError::PostInvalid { + source_code: "post_projection_diagnostic", + }); + } + match projection.classification() { + RadrootsPostClassification::Ask => Ok(RadrootsPhase1PublicationLeaf::Ask), + RadrootsPostClassification::PhotoUpdate => { + Ok(RadrootsPhase1PublicationLeaf::PhotoUpdate) + } + RadrootsPostClassification::Update => Ok(RadrootsPhase1PublicationLeaf::Update), + RadrootsPostClassification::ThreadExcluded => { + Err(RadrootsPhase1PublicationAllowlistError::PostExcluded) + } + } + } + KIND_CALENDAR_DATE_EVENT => { + validate_calendar_date(draft, artifact.media_references()) + .map_err(|_| RadrootsPhase1PublicationAllowlistError::EventInvalid)?; + Ok(RadrootsPhase1PublicationLeaf::EventDate) + } + KIND_CALENDAR_TIME_EVENT => { + validate_calendar_time(draft, artifact.media_references()) + .map_err(|_| RadrootsPhase1PublicationAllowlistError::EventInvalid)?; + Ok(RadrootsPhase1PublicationLeaf::EventTime) + } + KIND_CLASSIFIED_LISTING => reproject_food_availability(artifact), + kind => Err(RadrootsPhase1PublicationAllowlistError::UnsupportedKind { kind }), + } +} + +fn reproject_food_availability( + artifact: &RadrootsPhase1PublicationArtifact, +) -> Result<RadrootsPhase1PublicationLeaf, RadrootsPhase1PublicationAllowlistError> { + let draft = artifact.draft(); + let tags = RadrootsEventTags::new(draft.tags().to_vec()) + .map_err(|_| RadrootsPhase1PublicationAllowlistError::FoodInvalid)?; + let projection = project_inbound_food_availability_parts( + draft.kind(), + draft.created_at(), + &tags, + draft.content(), + ) + .map_err( + |error| RadrootsPhase1PublicationAllowlistError::FoodInvalidWithSource { + source_code: error.code(), + }, + )?; + match projection { + RadrootsFoodAvailabilityProjectionOutcome::Focused(projection) => { + if !projection.diagnostics().is_empty() { + return Err(RadrootsPhase1PublicationAllowlistError::FoodInvalid); + } + validate_food_availability(draft, artifact.media_references()) + .map_err(|_| RadrootsPhase1PublicationAllowlistError::FoodInvalid)?; + Ok(RadrootsPhase1PublicationLeaf::FoodAvailability) + } + RadrootsFoodAvailabilityProjectionOutcome::Excluded(partition) => { + Err(RadrootsPhase1PublicationAllowlistError::FoodExcluded { partition }) + } + } +} + +#[non_exhaustive] +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RadrootsPhase1PublicationAllowlistError { + ArtifactInvalid(RadrootsPhase1PublicationArtifactError), + UnsupportedKind { + kind: u32, + }, + ProfileInvalid, + PostInvalid { + source_code: &'static str, + }, + PostExcluded, + EventInvalid, + FoodInvalid, + FoodInvalidWithSource { + source_code: &'static str, + }, + FoodExcluded { + partition: RadrootsClassifiedListingPartition, + }, + SemanticVariantMismatch { + claimed: RadrootsPhase1PublicationLeaf, + projected: RadrootsPhase1PublicationLeaf, + }, +} + +impl RadrootsPhase1PublicationAllowlistError { + pub const fn code(&self) -> &'static str { + match self { + Self::ArtifactInvalid(_) => "publication_allowlist_artifact_invalid", + Self::UnsupportedKind { .. } => "publication_allowlist_kind_unsupported", + Self::ProfileInvalid => "publication_allowlist_profile_invalid", + Self::PostInvalid { .. } => "publication_allowlist_post_invalid", + Self::PostExcluded => "publication_allowlist_post_excluded", + Self::EventInvalid => "publication_allowlist_event_invalid", + Self::FoodInvalid | Self::FoodInvalidWithSource { .. } => { + "publication_allowlist_food_invalid" + } + Self::FoodExcluded { .. } => "publication_allowlist_food_excluded", + Self::SemanticVariantMismatch { .. } => { + "publication_allowlist_semantic_variant_mismatch" + } + } + } + + pub const fn source_code(&self) -> Option<&'static str> { + match self { + Self::ArtifactInvalid(error) => Some(error.code()), + Self::PostInvalid { source_code } => Some(*source_code), + Self::FoodInvalidWithSource { source_code } => Some(*source_code), + _ => None, + } + } +} + +impl fmt::Display for RadrootsPhase1PublicationAllowlistError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnsupportedKind { kind } => { + write!( + formatter, + "event kind {kind} is outside the Phase 1 publication allowlist" + ) + } + Self::FoodExcluded { partition } => write!( + formatter, + "classified-listing partition {partition:?} is outside the Phase 1 publication allowlist" + ), + Self::SemanticVariantMismatch { claimed, projected } => write!( + formatter, + "publication artifact claims {claimed} but reprojects as {projected}" + ), + error => formatter.write_str(error.code()), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for RadrootsPhase1PublicationAllowlistError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::ArtifactInvalid(error) => Some(error), + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::Value; + + const PREDECESSOR_VECTOR: &str = + include_str!("../../../tests/fixtures/phase1_publication_artifact.v1.json"); + + #[test] + fn publication_allowlist_admits_exactly_the_seven_sealed_leaves() { + let fixtures = [ + ("profile", RadrootsPhase1PublicationLeaf::Profile), + ("update", RadrootsPhase1PublicationLeaf::Update), + ("photo_update", RadrootsPhase1PublicationLeaf::PhotoUpdate), + ("ask", RadrootsPhase1PublicationLeaf::Ask), + ("event_date", RadrootsPhase1PublicationLeaf::EventDate), + ("event_time", RadrootsPhase1PublicationLeaf::EventTime), + ( + "food_availability", + RadrootsPhase1PublicationLeaf::FoodAvailability, + ), + ]; + + for (fixture, expected_leaf) in fixtures { + let artifact = artifact_fixture(fixture); + let canonical = artifact.to_canonical_json(); + let allowed = allow_phase1_publication_artifact(artifact).unwrap(); + assert_eq!(allowed.leaf(), expected_leaf); + assert_eq!(allowed.leaf().as_str(), fixture); + assert_eq!(allowed.leaf().kind(), allowed.artifact().draft().kind()); + assert_eq!( + allowed.leaf().authored_operation_id(), + allowed.artifact().authored_operation_id() + ); + assert_eq!( + allowed.leaf().event_contract_id(), + allowed.artifact().event_contract_id() + ); + assert_eq!(allowed.into_artifact().to_canonical_json(), canonical); + } + } + + #[test] + fn publication_allowlist_canonical_reload_uses_the_same_authority() { + for fixture in [ + "profile", + "update", + "photo_update", + "ask", + "event_date", + "event_time", + "food_availability", + ] { + let artifact = artifact_fixture(fixture); + let expected = allow_phase1_publication_artifact(artifact.clone()).unwrap(); + let actual = + allow_phase1_publication_canonical_json(&artifact.to_canonical_json()).unwrap(); + assert_eq!(actual, expected); + } + + let raw_event = br#"{"id":"00","pubkey":"00","created_at":1,"kind":1,"tags":[],"content":"raw","sig":"00"}"#; + assert_eq!( + allow_phase1_publication_canonical_json(raw_event) + .unwrap_err() + .source_code(), + Some("publication_artifact_invalid_json") + ); + } + + #[test] + fn publication_allowlist_reprojects_kind_one_with_exact_precedence() { + let update = artifact_fixture("update"); + assert_eq!( + reproject_publication_leaf(&update).unwrap(), + RadrootsPhase1PublicationLeaf::Update + ); + + let photo = artifact_fixture("photo_update"); + assert_eq!( + reproject_publication_leaf(&photo).unwrap(), + RadrootsPhase1PublicationLeaf::PhotoUpdate + ); + + let ask = artifact_fixture("ask"); + assert_eq!( + reproject_publication_leaf(&ask).unwrap(), + RadrootsPhase1PublicationLeaf::Ask + ); + + let mut ask_over_photo = photo; + ask_over_photo + .draft + .tags + .insert(0, vec!["t".to_owned(), "radroots-ask".to_owned()]); + assert_eq!( + reproject_publication_leaf(&ask_over_photo).unwrap(), + RadrootsPhase1PublicationLeaf::Ask + ); + + let mut reply = update; + reply.draft.tags = vec![vec!["e".to_owned(), "00".repeat(32)]]; + assert_eq!( + reproject_publication_leaf(&reply).unwrap_err(), + RadrootsPhase1PublicationAllowlistError::PostExcluded + ); + } + + #[test] + fn publication_allowlist_partitions_food_markers_before_profile_validation() { + let focused = artifact_fixture("food_availability"); + assert_eq!( + reproject_publication_leaf(&focused).unwrap(), + RadrootsPhase1PublicationLeaf::FoodAvailability + ); + + let mut generic = focused.clone(); + generic.draft.tags.retain(|tag| { + !matches!( + tag.first().map(String::as_str), + Some("radroots:price_unit" | "radroots:quantity") + ) + }); + assert_eq!( + reproject_publication_leaf(&generic).unwrap_err(), + RadrootsPhase1PublicationAllowlistError::FoodExcluded { + partition: RadrootsClassifiedListingPartition::GenericNip99, + } + ); + + let mut operational = generic; + operational.draft.tags.push(vec![ + "radroots:price".to_owned(), + "3".to_owned(), + "CAD".to_owned(), + ]); + assert_eq!( + reproject_publication_leaf(&operational).unwrap_err(), + RadrootsPhase1PublicationAllowlistError::FoodExcluded { + partition: RadrootsClassifiedListingPartition::OperationalListing, + } + ); + + let mut mixed = focused; + mixed.draft.tags.push(vec![ + "radroots:price".to_owned(), + "3".to_owned(), + "CAD".to_owned(), + ]); + assert_eq!( + reproject_publication_leaf(&mixed).unwrap_err().code(), + "publication_allowlist_food_invalid" + ); + } + + #[test] + fn publication_allowlist_rejects_generic_calendar_and_unsupported_kinds() { + let mut generic_calendar = artifact_fixture("event_date"); + generic_calendar.draft.tags = vec![ + vec!["d".to_owned(), "generic-calendar".to_owned()], + vec!["title".to_owned(), "Generic calendar event".to_owned()], + ]; + assert_eq!( + reproject_publication_leaf(&generic_calendar).unwrap_err(), + RadrootsPhase1PublicationAllowlistError::EventInvalid + ); + + let mut bud11 = artifact_fixture("update"); + bud11.draft.kind = 24_242; + assert_eq!( + reproject_publication_leaf(&bud11).unwrap_err(), + RadrootsPhase1PublicationAllowlistError::UnsupportedKind { kind: 24_242 } + ); + + let mut ephemeral = artifact_fixture("update"); + ephemeral.draft.kind = 20_001; + assert_eq!( + reproject_publication_leaf(&ephemeral).unwrap_err(), + RadrootsPhase1PublicationAllowlistError::UnsupportedKind { kind: 20_001 } + ); + } + + fn artifact_fixture(name: &str) -> RadrootsPhase1PublicationArtifact { + let suite: Value = serde_json::from_str(PREDECESSOR_VECTOR).unwrap(); + let vector = suite["vectors"] + .as_array() + .unwrap() + .iter() + .find(|vector| vector["expected"]["semantic_variant"].as_str() == Some(name)) + .unwrap_or_else(|| panic!("missing predecessor fixture {name}")); + let canonical = vector["expected"]["canonical_json"] + .as_str() + .unwrap_or_else(|| panic!("missing canonical predecessor fixture {name}")); + RadrootsPhase1PublicationArtifact::from_canonical_json(canonical.as_bytes()).unwrap() + } +} diff --git a/crates/event_codec/tests/fixtures/phase1_publication_allowlist.v1.json b/crates/event_codec/tests/fixtures/phase1_publication_allowlist.v1.json @@ -0,0 +1,549 @@ +{ + "contract_version": "1.0.0", + "suite": "phase1_publication_allowlist", + "vectors": [ + { + "expected": { + "decision": "allow", + "event_kind": 0, + "leaf": "profile" + }, + "id": "sealed_profile_is_allowed", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"profile\",\"authored_operation_id\":\"profile.build_authored_draft\",\"event_contract_id\":\"radroots.profile.metadata.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":0,\"tags\":[],\"content\":\"{\\\"name\\\":\\\"victoria-farm\\\",\\\"display_name\\\":\\\"Victoria Farm\\\",\\\"about\\\":\\\"Seasonal produce from the Saanich Peninsula\\\",\\\"picture\\\":\\\"https://media.example/512f6a371c77694502e7d08f0b1f1080c7103ca90925bfe2fa23106aac11003a.png\\\",\\\"banner\\\":\\\"https://media.example/fbf3819415a76ea0d3ba71817578bb89c4903aa958e38f76f86578dfa8a35145.webp\\\",\\\"nip05\\\":\\\"farm@example.com\\\",\\\"bot\\\":false}\"},\"expected_event_id\":\"a7d081b9c142e3a68245bafe1921b0e0dd88f886406e7165a9729f6c57026289\",\"media_references\":[{\"url\":\"https://media.example/512f6a371c77694502e7d08f0b1f1080c7103ca90925bfe2fa23106aac11003a.png\",\"sha256\":\"512f6a371c77694502e7d08f0b1f1080c7103ca90925bfe2fa23106aac11003a\",\"size\":15,\"media_type\":\"image/png\"},{\"url\":\"https://media.example/fbf3819415a76ea0d3ba71817578bb89c4903aa958e38f76f86578dfa8a35145.webp\",\"sha256\":\"fbf3819415a76ea0d3ba71817578bb89c4903aa958e38f76f86578dfa8a35145\",\"size\":14,\"media_type\":\"image/webp\"}],\"artifact_digest\":\"e296ed63312d58b4e3ee160d4ca3457eb922920d3116329ca498f08e402215b0\"}", + "surface": "sealed_artifact_json" + }, + "kind": "publication_allowlist.allow_artifact.valid" + }, + { + "expected": { + "decision": "allow", + "event_kind": 1, + "leaf": "update" + }, + "id": "sealed_update_is_allowed", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"update\",\"authored_operation_id\":\"social.update.build_authored_draft\",\"event_contract_id\":\"radroots.social.update.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":1,\"tags\":[],\"content\":\"Carrots harvested today\"},\"expected_event_id\":\"7f2d9dbbd6d2f3db1e83338ea2e669b2458e62ded159de15a3fa98dcf9f164e3\",\"media_references\":[],\"artifact_digest\":\"9ad318496bd4a710fbc7f3e0f6d5a01de808d352db91ca56a12e710904784cc5\"}", + "surface": "sealed_artifact_json" + }, + "kind": "publication_allowlist.allow_artifact.valid" + }, + { + "expected": { + "decision": "allow", + "event_kind": 1, + "leaf": "photo_update" + }, + "id": "sealed_photo_update_is_allowed", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"photo_update\",\"authored_operation_id\":\"social.photo_update.build_authored_draft\",\"event_contract_id\":\"radroots.social.photo_update.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":1,\"tags\":[[\"imeta\",\"url https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"x 51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"m image/webp\",\"dim 1200x900\",\"size 13\",\"alt Fresh strawberries\",\"fallback https://backup.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\"]],\"content\":\"Strawberries at the farm stand https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\"},\"expected_event_id\":\"e592229776cd2a0d0e25a701f4629c0a5dc6e08481a235623d50da6ee8e8f438\",\"media_references\":[{\"url\":\"https://backup.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"sha256\":\"51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"size\":13,\"media_type\":\"image/webp\"},{\"url\":\"https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"sha256\":\"51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"size\":13,\"media_type\":\"image/webp\"}],\"artifact_digest\":\"9ed6b7a6fcec3b07bd0c9179fd5b6ecf74517390051e3c2bd1659be62a26c197\"}", + "surface": "sealed_artifact_json" + }, + "kind": "publication_allowlist.allow_artifact.valid" + }, + { + "expected": { + "decision": "allow", + "event_kind": 1, + "leaf": "ask" + }, + "id": "sealed_ask_is_allowed", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"ask\",\"authored_operation_id\":\"social.ask.build_authored_draft\",\"event_contract_id\":\"radroots.social.ask.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":1,\"tags\":[[\"t\",\"radroots-ask\"],[\"imeta\",\"url https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"x 51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"m image/webp\",\"dim 1200x900\",\"size 13\",\"alt Fresh strawberries\",\"fallback https://backup.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\"]],\"content\":\"When will strawberries be ready? https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\"},\"expected_event_id\":\"bd45fbbb5bfb8a5aa32df81d240e5f3bd56b0f8cde3a20b3f7d455801373fe48\",\"media_references\":[{\"url\":\"https://backup.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"sha256\":\"51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"size\":13,\"media_type\":\"image/webp\"},{\"url\":\"https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"sha256\":\"51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"size\":13,\"media_type\":\"image/webp\"}],\"artifact_digest\":\"41eddea199bba97adebef4ce433ebfa05d97978ce8464ed3d52762ab717c2a3c\"}", + "surface": "sealed_artifact_json" + }, + "kind": "publication_allowlist.allow_artifact.valid" + }, + { + "expected": { + "decision": "allow", + "event_kind": 31922, + "leaf": "event_date" + }, + "id": "sealed_date_event_is_allowed", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"event_date\",\"authored_operation_id\":\"social.calendar_date_event.build_authored_draft\",\"event_contract_id\":\"radroots.calendar.date_event.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":31922,\"tags\":[[\"d\",\"farmers-market-2026\"],[\"title\",\"Moss Street Farmers Market\"],[\"start\",\"2026-07-25\"],[\"end\",\"2026-07-26\"],[\"location\",\"Victoria, BC\"],[\"image\",\"https://events.example/0a422cbf828d421341c40c678f4cfbd6451841760db126e5f5ac3d2e06fd80b8.jpeg\"]],\"content\":\"Saturday market in Victoria\"},\"expected_event_id\":\"bea82662b6acf2f1272c8335a24bb0957e35d22c2b29d3e30c094ad7398dc7c7\",\"media_references\":[{\"url\":\"https://events.example/0a422cbf828d421341c40c678f4cfbd6451841760db126e5f5ac3d2e06fd80b8.jpeg\",\"sha256\":\"0a422cbf828d421341c40c678f4cfbd6451841760db126e5f5ac3d2e06fd80b8\",\"size\":10,\"media_type\":\"image/jpeg\"}],\"artifact_digest\":\"0ec5162ab80988999aac62329d4cfc9c7efce6a82bbda539a723347d73596eb3\"}", + "surface": "sealed_artifact_json" + }, + "kind": "publication_allowlist.allow_artifact.valid" + }, + { + "expected": { + "decision": "allow", + "event_kind": 31923, + "leaf": "event_time" + }, + "id": "sealed_time_event_is_allowed", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"event_time\",\"authored_operation_id\":\"social.calendar_time_event.build_authored_draft\",\"event_contract_id\":\"radroots.calendar.time_event.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":31923,\"tags\":[[\"d\",\"farm-tour-2026\"],[\"title\",\"Saanich Farm Tour\"],[\"start\",\"1785003600\"],[\"end\",\"1785007200\"],[\"D\",\"20659\"],[\"start_tzid\",\"America/Vancouver\"],[\"image\",\"https://events.example/0a422cbf828d421341c40c678f4cfbd6451841760db126e5f5ac3d2e06fd80b8.jpeg\"]],\"content\":\"A one-hour farm tour\"},\"expected_event_id\":\"2088ed11cd8c1495a82e6f5198ed2ee98e0cbc599f22d7f3380892155dc55894\",\"media_references\":[{\"url\":\"https://events.example/0a422cbf828d421341c40c678f4cfbd6451841760db126e5f5ac3d2e06fd80b8.jpeg\",\"sha256\":\"0a422cbf828d421341c40c678f4cfbd6451841760db126e5f5ac3d2e06fd80b8\",\"size\":10,\"media_type\":\"image/jpeg\"}],\"artifact_digest\":\"56bfb4bed87559a58922343905a73c0c96dd0a57f94ff0955caecf0589198638\"}", + "surface": "sealed_artifact_json" + }, + "kind": "publication_allowlist.allow_artifact.valid" + }, + { + "expected": { + "decision": "allow", + "event_kind": 30402, + "leaf": "food_availability" + }, + "id": "sealed_food_availability_is_allowed", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"food_availability\",\"authored_operation_id\":\"food_availability.build_authored_draft\",\"event_contract_id\":\"radroots.food.availability.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":30402,\"tags\":[[\"d\",\"nantes-carrots\"],[\"title\",\"Nantes Carrots\"],[\"summary\",\"Fresh bunches\"],[\"published_at\",\"1784347140\"],[\"location\",\"Central Saanich, BC\"],[\"price\",\"3\",\"CAD\"],[\"radroots:price_unit\",\"lb\"],[\"radroots:quantity\",\"24\",\"lb\"],[\"status\",\"active\"],[\"image\",\"https://food.example/8a66441b557b29193781023160b0216c5518e9b84749f29ecf118a728f451a5e.png\",\"1200x800\"]],\"content\":\"Fresh Nantes carrots available this week.\"},\"expected_event_id\":\"76b60f1b178c0dbdc14a87de36151ea6b04a866ce50e34a0ccae2f406f514e81\",\"media_references\":[{\"url\":\"https://food.example/8a66441b557b29193781023160b0216c5518e9b84749f29ecf118a728f451a5e.png\",\"sha256\":\"8a66441b557b29193781023160b0216c5518e9b84749f29ecf118a728f451a5e\",\"size\":14,\"media_type\":\"image/png\"}],\"artifact_digest\":\"f0989cb993194af4a2f907ad8337e50378d2757c536a09afb4055a681043a7d8\"}", + "surface": "sealed_artifact_json" + }, + "kind": "publication_allowlist.allow_artifact.valid" + }, + { + "expected": { + "decision": "allow", + "event_kind": 0, + "leaf": "profile" + }, + "id": "canonical_profile_is_allowed", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"profile\",\"authored_operation_id\":\"profile.build_authored_draft\",\"event_contract_id\":\"radroots.profile.metadata.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":0,\"tags\":[],\"content\":\"{\\\"name\\\":\\\"victoria-farm\\\",\\\"display_name\\\":\\\"Victoria Farm\\\",\\\"about\\\":\\\"Seasonal produce from the Saanich Peninsula\\\",\\\"picture\\\":\\\"https://media.example/512f6a371c77694502e7d08f0b1f1080c7103ca90925bfe2fa23106aac11003a.png\\\",\\\"banner\\\":\\\"https://media.example/fbf3819415a76ea0d3ba71817578bb89c4903aa958e38f76f86578dfa8a35145.webp\\\",\\\"nip05\\\":\\\"farm@example.com\\\",\\\"bot\\\":false}\"},\"expected_event_id\":\"a7d081b9c142e3a68245bafe1921b0e0dd88f886406e7165a9729f6c57026289\",\"media_references\":[{\"url\":\"https://media.example/512f6a371c77694502e7d08f0b1f1080c7103ca90925bfe2fa23106aac11003a.png\",\"sha256\":\"512f6a371c77694502e7d08f0b1f1080c7103ca90925bfe2fa23106aac11003a\",\"size\":15,\"media_type\":\"image/png\"},{\"url\":\"https://media.example/fbf3819415a76ea0d3ba71817578bb89c4903aa958e38f76f86578dfa8a35145.webp\",\"sha256\":\"fbf3819415a76ea0d3ba71817578bb89c4903aa958e38f76f86578dfa8a35145\",\"size\":14,\"media_type\":\"image/webp\"}],\"artifact_digest\":\"e296ed63312d58b4e3ee160d4ca3457eb922920d3116329ca498f08e402215b0\"}", + "surface": "canonical_artifact_json" + }, + "kind": "publication_allowlist.allow_canonical_json.valid" + }, + { + "expected": { + "decision": "allow", + "event_kind": 1, + "leaf": "update" + }, + "id": "canonical_update_is_allowed", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"update\",\"authored_operation_id\":\"social.update.build_authored_draft\",\"event_contract_id\":\"radroots.social.update.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":1,\"tags\":[],\"content\":\"Carrots harvested today\"},\"expected_event_id\":\"7f2d9dbbd6d2f3db1e83338ea2e669b2458e62ded159de15a3fa98dcf9f164e3\",\"media_references\":[],\"artifact_digest\":\"9ad318496bd4a710fbc7f3e0f6d5a01de808d352db91ca56a12e710904784cc5\"}", + "surface": "canonical_artifact_json" + }, + "kind": "publication_allowlist.allow_canonical_json.valid" + }, + { + "expected": { + "decision": "allow", + "event_kind": 1, + "leaf": "photo_update" + }, + "id": "canonical_photo_update_is_allowed", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"photo_update\",\"authored_operation_id\":\"social.photo_update.build_authored_draft\",\"event_contract_id\":\"radroots.social.photo_update.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":1,\"tags\":[[\"imeta\",\"url https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"x 51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"m image/webp\",\"dim 1200x900\",\"size 13\",\"alt Fresh strawberries\",\"fallback https://backup.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\"]],\"content\":\"Strawberries at the farm stand https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\"},\"expected_event_id\":\"e592229776cd2a0d0e25a701f4629c0a5dc6e08481a235623d50da6ee8e8f438\",\"media_references\":[{\"url\":\"https://backup.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"sha256\":\"51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"size\":13,\"media_type\":\"image/webp\"},{\"url\":\"https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"sha256\":\"51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"size\":13,\"media_type\":\"image/webp\"}],\"artifact_digest\":\"9ed6b7a6fcec3b07bd0c9179fd5b6ecf74517390051e3c2bd1659be62a26c197\"}", + "surface": "canonical_artifact_json" + }, + "kind": "publication_allowlist.allow_canonical_json.valid" + }, + { + "expected": { + "decision": "allow", + "event_kind": 1, + "leaf": "ask" + }, + "id": "canonical_ask_is_allowed", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"ask\",\"authored_operation_id\":\"social.ask.build_authored_draft\",\"event_contract_id\":\"radroots.social.ask.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":1,\"tags\":[[\"t\",\"radroots-ask\"],[\"imeta\",\"url https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"x 51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"m image/webp\",\"dim 1200x900\",\"size 13\",\"alt Fresh strawberries\",\"fallback https://backup.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\"]],\"content\":\"When will strawberries be ready? https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\"},\"expected_event_id\":\"bd45fbbb5bfb8a5aa32df81d240e5f3bd56b0f8cde3a20b3f7d455801373fe48\",\"media_references\":[{\"url\":\"https://backup.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"sha256\":\"51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"size\":13,\"media_type\":\"image/webp\"},{\"url\":\"https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"sha256\":\"51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"size\":13,\"media_type\":\"image/webp\"}],\"artifact_digest\":\"41eddea199bba97adebef4ce433ebfa05d97978ce8464ed3d52762ab717c2a3c\"}", + "surface": "canonical_artifact_json" + }, + "kind": "publication_allowlist.allow_canonical_json.valid" + }, + { + "expected": { + "decision": "allow", + "event_kind": 31922, + "leaf": "event_date" + }, + "id": "canonical_date_event_is_allowed", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"event_date\",\"authored_operation_id\":\"social.calendar_date_event.build_authored_draft\",\"event_contract_id\":\"radroots.calendar.date_event.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":31922,\"tags\":[[\"d\",\"farmers-market-2026\"],[\"title\",\"Moss Street Farmers Market\"],[\"start\",\"2026-07-25\"],[\"end\",\"2026-07-26\"],[\"location\",\"Victoria, BC\"],[\"image\",\"https://events.example/0a422cbf828d421341c40c678f4cfbd6451841760db126e5f5ac3d2e06fd80b8.jpeg\"]],\"content\":\"Saturday market in Victoria\"},\"expected_event_id\":\"bea82662b6acf2f1272c8335a24bb0957e35d22c2b29d3e30c094ad7398dc7c7\",\"media_references\":[{\"url\":\"https://events.example/0a422cbf828d421341c40c678f4cfbd6451841760db126e5f5ac3d2e06fd80b8.jpeg\",\"sha256\":\"0a422cbf828d421341c40c678f4cfbd6451841760db126e5f5ac3d2e06fd80b8\",\"size\":10,\"media_type\":\"image/jpeg\"}],\"artifact_digest\":\"0ec5162ab80988999aac62329d4cfc9c7efce6a82bbda539a723347d73596eb3\"}", + "surface": "canonical_artifact_json" + }, + "kind": "publication_allowlist.allow_canonical_json.valid" + }, + { + "expected": { + "decision": "allow", + "event_kind": 31923, + "leaf": "event_time" + }, + "id": "canonical_time_event_is_allowed", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"event_time\",\"authored_operation_id\":\"social.calendar_time_event.build_authored_draft\",\"event_contract_id\":\"radroots.calendar.time_event.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":31923,\"tags\":[[\"d\",\"farm-tour-2026\"],[\"title\",\"Saanich Farm Tour\"],[\"start\",\"1785003600\"],[\"end\",\"1785007200\"],[\"D\",\"20659\"],[\"start_tzid\",\"America/Vancouver\"],[\"image\",\"https://events.example/0a422cbf828d421341c40c678f4cfbd6451841760db126e5f5ac3d2e06fd80b8.jpeg\"]],\"content\":\"A one-hour farm tour\"},\"expected_event_id\":\"2088ed11cd8c1495a82e6f5198ed2ee98e0cbc599f22d7f3380892155dc55894\",\"media_references\":[{\"url\":\"https://events.example/0a422cbf828d421341c40c678f4cfbd6451841760db126e5f5ac3d2e06fd80b8.jpeg\",\"sha256\":\"0a422cbf828d421341c40c678f4cfbd6451841760db126e5f5ac3d2e06fd80b8\",\"size\":10,\"media_type\":\"image/jpeg\"}],\"artifact_digest\":\"56bfb4bed87559a58922343905a73c0c96dd0a57f94ff0955caecf0589198638\"}", + "surface": "canonical_artifact_json" + }, + "kind": "publication_allowlist.allow_canonical_json.valid" + }, + { + "expected": { + "decision": "allow", + "event_kind": 30402, + "leaf": "food_availability" + }, + "id": "canonical_food_availability_is_allowed", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"food_availability\",\"authored_operation_id\":\"food_availability.build_authored_draft\",\"event_contract_id\":\"radroots.food.availability.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":30402,\"tags\":[[\"d\",\"nantes-carrots\"],[\"title\",\"Nantes Carrots\"],[\"summary\",\"Fresh bunches\"],[\"published_at\",\"1784347140\"],[\"location\",\"Central Saanich, BC\"],[\"price\",\"3\",\"CAD\"],[\"radroots:price_unit\",\"lb\"],[\"radroots:quantity\",\"24\",\"lb\"],[\"status\",\"active\"],[\"image\",\"https://food.example/8a66441b557b29193781023160b0216c5518e9b84749f29ecf118a728f451a5e.png\",\"1200x800\"]],\"content\":\"Fresh Nantes carrots available this week.\"},\"expected_event_id\":\"76b60f1b178c0dbdc14a87de36151ea6b04a866ce50e34a0ccae2f406f514e81\",\"media_references\":[{\"url\":\"https://food.example/8a66441b557b29193781023160b0216c5518e9b84749f29ecf118a728f451a5e.png\",\"sha256\":\"8a66441b557b29193781023160b0216c5518e9b84749f29ecf118a728f451a5e\",\"size\":14,\"media_type\":\"image/png\"}],\"artifact_digest\":\"f0989cb993194af4a2f907ad8337e50378d2757c536a09afb4055a681043a7d8\"}", + "surface": "canonical_artifact_json" + }, + "kind": "publication_allowlist.allow_canonical_json.valid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_post_profile_invalid" + }, + "id": "update_claim_with_ask_marker_is_rejected", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"update\",\"authored_operation_id\":\"social.update.build_authored_draft\",\"event_contract_id\":\"radroots.social.update.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":1,\"tags\":[[\"t\",\"radroots-ask\"]],\"content\":\"Carrots harvested today\"},\"expected_event_id\":\"bce463fddb9395ba11d4b8500379a39e14afd1472b3cf47e22b6b9f2a438353e\",\"media_references\":[],\"artifact_digest\":\"0000000000000000000000000000000000000000000000000000000000000000\"}", + "surface": "artifact_candidate_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_post_profile_invalid" + }, + "id": "photo_claim_with_ask_marker_is_rejected", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"photo_update\",\"authored_operation_id\":\"social.photo_update.build_authored_draft\",\"event_contract_id\":\"radroots.social.photo_update.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":1,\"tags\":[[\"t\",\"radroots-ask\"],[\"imeta\",\"url https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"x 51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"m image/webp\",\"dim 1200x900\",\"size 13\",\"alt Fresh strawberries\",\"fallback https://backup.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\"]],\"content\":\"Strawberries at the farm stand https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\"},\"expected_event_id\":\"57afde82467e5d72aa07ee8cf1db454945a1615295e4e112d9ffd89e987c10f4\",\"media_references\":[{\"url\":\"https://backup.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"sha256\":\"51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"size\":13,\"media_type\":\"image/webp\"},{\"url\":\"https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"sha256\":\"51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"size\":13,\"media_type\":\"image/webp\"}],\"artifact_digest\":\"0000000000000000000000000000000000000000000000000000000000000000\"}", + "surface": "artifact_candidate_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_post_profile_invalid" + }, + "id": "ask_claim_without_marker_is_rejected", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"ask\",\"authored_operation_id\":\"social.ask.build_authored_draft\",\"event_contract_id\":\"radroots.social.ask.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":1,\"tags\":[[\"imeta\",\"url https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"x 51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"m image/webp\",\"dim 1200x900\",\"size 13\",\"alt Fresh strawberries\",\"fallback https://backup.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\"]],\"content\":\"When will strawberries be ready? https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\"},\"expected_event_id\":\"3f67e9ff31c26764bc1c74ef9cafd570a7a7454b3e3768be8f8c1a6175b3d7db\",\"media_references\":[{\"url\":\"https://backup.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"sha256\":\"51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"size\":13,\"media_type\":\"image/webp\"},{\"url\":\"https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"sha256\":\"51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"size\":13,\"media_type\":\"image/webp\"}],\"artifact_digest\":\"0000000000000000000000000000000000000000000000000000000000000000\"}", + "surface": "artifact_candidate_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_post_profile_invalid" + }, + "id": "update_claim_with_photo_profile_is_rejected", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"update\",\"authored_operation_id\":\"social.update.build_authored_draft\",\"event_contract_id\":\"radroots.social.update.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":1,\"tags\":[[\"imeta\",\"url https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"x 51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"m image/webp\",\"dim 1200x900\",\"size 13\",\"alt Fresh strawberries\",\"fallback https://backup.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\"]],\"content\":\"Strawberries at the farm stand https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\"},\"expected_event_id\":\"e592229776cd2a0d0e25a701f4629c0a5dc6e08481a235623d50da6ee8e8f438\",\"media_references\":[{\"url\":\"https://backup.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"sha256\":\"51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"size\":13,\"media_type\":\"image/webp\"},{\"url\":\"https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp\",\"sha256\":\"51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0\",\"size\":13,\"media_type\":\"image/webp\"}],\"artifact_digest\":\"0000000000000000000000000000000000000000000000000000000000000000\"}", + "surface": "artifact_candidate_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_calendar_profile_invalid" + }, + "id": "typed_date_event_with_generic_shape_is_rejected", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"event_date\",\"authored_operation_id\":\"social.calendar_date_event.build_authored_draft\",\"event_contract_id\":\"radroots.calendar.date_event.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":31922,\"tags\":[[\"d\",\"generic-calendar\"],[\"title\",\"Generic calendar event\"]],\"content\":\"Saturday market in Victoria\"},\"expected_event_id\":\"10e28f8f61a9bb9affa110bb6cce7cc8e371918aead30fbd31d41792db8f3325\",\"media_references\":[],\"artifact_digest\":\"0000000000000000000000000000000000000000000000000000000000000000\"}", + "family": "generic_calendar", + "surface": "artifact_candidate_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_calendar_profile_invalid" + }, + "id": "typed_time_event_with_generic_shape_is_rejected", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"event_time\",\"authored_operation_id\":\"social.calendar_time_event.build_authored_draft\",\"event_contract_id\":\"radroots.calendar.time_event.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":31923,\"tags\":[[\"d\",\"generic-calendar\"],[\"title\",\"Generic calendar event\"]],\"content\":\"A one-hour farm tour\"},\"expected_event_id\":\"c3da57e519122044ee663cece433d56f0b5d967f8e78a1a1a80260ae7653d6bd\",\"media_references\":[],\"artifact_digest\":\"0000000000000000000000000000000000000000000000000000000000000000\"}", + "family": "generic_calendar", + "surface": "artifact_candidate_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_food_profile_invalid" + }, + "id": "operational_listing_profile_is_rejected", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"food_availability\",\"authored_operation_id\":\"food_availability.build_authored_draft\",\"event_contract_id\":\"radroots.food.availability.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":30402,\"tags\":[[\"d\",\"nantes-carrots\"],[\"title\",\"Nantes Carrots\"],[\"summary\",\"Fresh bunches\"],[\"published_at\",\"1784347140\"],[\"location\",\"Central Saanich, BC\"],[\"price\",\"3\",\"CAD\"],[\"status\",\"active\"],[\"image\",\"https://food.example/8a66441b557b29193781023160b0216c5518e9b84749f29ecf118a728f451a5e.png\",\"1200x800\"],[\"radroots:price\",\"3\",\"CAD\"]],\"content\":\"Fresh Nantes carrots available this week.\"},\"expected_event_id\":\"d752583639bf9edaa5ab25e491be5b565b91af5fb01167377d36fdb319dd61d7\",\"media_references\":[{\"url\":\"https://food.example/8a66441b557b29193781023160b0216c5518e9b84749f29ecf118a728f451a5e.png\",\"sha256\":\"8a66441b557b29193781023160b0216c5518e9b84749f29ecf118a728f451a5e\",\"size\":14,\"media_type\":\"image/png\"}],\"artifact_digest\":\"0000000000000000000000000000000000000000000000000000000000000000\"}", + "family": "operational_listing", + "surface": "artifact_candidate_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_food_profile_invalid" + }, + "id": "generic_nip99_profile_is_rejected", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"food_availability\",\"authored_operation_id\":\"food_availability.build_authored_draft\",\"event_contract_id\":\"radroots.food.availability.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":30402,\"tags\":[[\"d\",\"nantes-carrots\"],[\"title\",\"Nantes Carrots\"],[\"summary\",\"Fresh bunches\"],[\"published_at\",\"1784347140\"],[\"location\",\"Central Saanich, BC\"],[\"price\",\"3\",\"CAD\"],[\"status\",\"active\"],[\"image\",\"https://food.example/8a66441b557b29193781023160b0216c5518e9b84749f29ecf118a728f451a5e.png\",\"1200x800\"]],\"content\":\"Fresh Nantes carrots available this week.\"},\"expected_event_id\":\"6ae040461fe7e6967bb48b2861bc1dbeac075f0331027e2d04b9618e2d752a2d\",\"media_references\":[{\"url\":\"https://food.example/8a66441b557b29193781023160b0216c5518e9b84749f29ecf118a728f451a5e.png\",\"sha256\":\"8a66441b557b29193781023160b0216c5518e9b84749f29ecf118a728f451a5e\",\"size\":14,\"media_type\":\"image/png\"}],\"artifact_digest\":\"0000000000000000000000000000000000000000000000000000000000000000\"}", + "family": "generic_nip99", + "surface": "artifact_candidate_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_food_profile_invalid" + }, + "id": "mixed_food_and_operational_markers_are_rejected", + "input": { + "canonical_json": "{\"schema_version\":1,\"semantic_variant\":\"food_availability\",\"authored_operation_id\":\"food_availability.build_authored_draft\",\"event_contract_id\":\"radroots.food.availability.v1\",\"expected_author\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"draft\":{\"created_at\":1784347200,\"kind\":30402,\"tags\":[[\"d\",\"nantes-carrots\"],[\"title\",\"Nantes Carrots\"],[\"summary\",\"Fresh bunches\"],[\"published_at\",\"1784347140\"],[\"location\",\"Central Saanich, BC\"],[\"price\",\"3\",\"CAD\"],[\"radroots:price_unit\",\"lb\"],[\"radroots:quantity\",\"24\",\"lb\"],[\"status\",\"active\"],[\"image\",\"https://food.example/8a66441b557b29193781023160b0216c5518e9b84749f29ecf118a728f451a5e.png\",\"1200x800\"],[\"radroots:price\",\"3\",\"CAD\"]],\"content\":\"Fresh Nantes carrots available this week.\"},\"expected_event_id\":\"5302117c4c89252c40b57601fe8b115b779bd9256235f449ad86c50ca97c8219\",\"media_references\":[{\"url\":\"https://food.example/8a66441b557b29193781023160b0216c5518e9b84749f29ecf118a728f451a5e.png\",\"sha256\":\"8a66441b557b29193781023160b0216c5518e9b84749f29ecf118a728f451a5e\",\"size\":14,\"media_type\":\"image/png\"}],\"artifact_digest\":\"0000000000000000000000000000000000000000000000000000000000000000\"}", + "family": "mixed_classified_listing", + "surface": "artifact_candidate_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_artifact_invalid_json" + }, + "id": "unsealed_profile_is_rejected", + "input": { + "canonical_json": "{\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"pubkey\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"created_at\":1784347200,\"kind\":0,\"tags\":[],\"content\":\"{\\\"name\\\":\\\"victoria-farm\\\",\\\"display_name\\\":\\\"Victoria Farm\\\",\\\"about\\\":\\\"Seasonal produce from the Saanich Peninsula\\\",\\\"picture\\\":\\\"https://media.example/512f6a371c77694502e7d08f0b1f1080c7103ca90925bfe2fa23106aac11003a.png\\\",\\\"banner\\\":\\\"https://media.example/fbf3819415a76ea0d3ba71817578bb89c4903aa958e38f76f86578dfa8a35145.webp\\\",\\\"nip05\\\":\\\"farm@example.com\\\",\\\"bot\\\":false}\",\"sig\":\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}", + "event_kind": 0, + "family": "unsealed_profile", + "surface": "raw_nip01_event_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_artifact_invalid_json" + }, + "id": "generic_root_note_is_rejected", + "input": { + "canonical_json": "{\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"pubkey\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"created_at\":1784347200,\"kind\":1,\"tags\":[],\"content\":\"Generic root note\",\"sig\":\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}", + "event_kind": 1, + "family": "generic_root_note", + "surface": "raw_nip01_event_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_artifact_invalid_json" + }, + "id": "reply_is_rejected", + "input": { + "canonical_json": "{\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"pubkey\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"created_at\":1784347200,\"kind\":1,\"tags\":[[\"e\",\"1111111111111111111111111111111111111111111111111111111111111111\",\"\",\"reply\"]],\"content\":\"Reply\",\"sig\":\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}", + "event_kind": 1, + "family": "reply", + "surface": "raw_nip01_event_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_artifact_invalid_json" + }, + "id": "comment_is_rejected", + "input": { + "canonical_json": "{\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"pubkey\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"created_at\":1784347200,\"kind\":1111,\"tags\":[[\"E\",\"1111111111111111111111111111111111111111111111111111111111111111\"]],\"content\":\"Comment\",\"sig\":\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}", + "event_kind": 1111, + "family": "comment", + "surface": "raw_nip01_event_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_artifact_invalid_json" + }, + "id": "deletion_request_is_rejected", + "input": { + "canonical_json": "{\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"pubkey\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"created_at\":1784347200,\"kind\":5,\"tags\":[[\"e\",\"1111111111111111111111111111111111111111111111111111111111111111\"]],\"content\":\"Removed\",\"sig\":\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}", + "event_kind": 5, + "family": "deletion_request", + "surface": "raw_nip01_event_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_artifact_invalid_json" + }, + "id": "raw_date_event_is_rejected", + "input": { + "canonical_json": "{\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"pubkey\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"created_at\":1784347200,\"kind\":31922,\"tags\":[[\"d\",\"farmers-market-2026\"],[\"title\",\"Moss Street Farmers Market\"],[\"start\",\"2026-07-25\"],[\"end\",\"2026-07-26\"],[\"location\",\"Victoria, BC\"],[\"image\",\"https://events.example/0a422cbf828d421341c40c678f4cfbd6451841760db126e5f5ac3d2e06fd80b8.jpeg\"]],\"content\":\"Saturday market in Victoria\",\"sig\":\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}", + "event_kind": 31922, + "family": "unsealed_calendar_event", + "surface": "raw_nip01_event_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_artifact_invalid_json" + }, + "id": "raw_time_event_is_rejected", + "input": { + "canonical_json": "{\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"pubkey\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"created_at\":1784347200,\"kind\":31923,\"tags\":[[\"d\",\"farm-tour-2026\"],[\"title\",\"Saanich Farm Tour\"],[\"start\",\"1785003600\"],[\"end\",\"1785007200\"],[\"D\",\"20659\"],[\"start_tzid\",\"America/Vancouver\"],[\"image\",\"https://events.example/0a422cbf828d421341c40c678f4cfbd6451841760db126e5f5ac3d2e06fd80b8.jpeg\"]],\"content\":\"A one-hour farm tour\",\"sig\":\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}", + "event_kind": 31923, + "family": "unsealed_calendar_event", + "surface": "raw_nip01_event_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_artifact_invalid_json" + }, + "id": "calendar_collection_is_rejected", + "input": { + "canonical_json": "{\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"pubkey\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"created_at\":1784347200,\"kind\":31924,\"tags\":[[\"d\",\"victoria-calendar\"]],\"content\":\"\",\"sig\":\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}", + "event_kind": 31924, + "family": "calendar_collection", + "surface": "raw_nip01_event_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_artifact_invalid_json" + }, + "id": "calendar_rsvp_is_rejected", + "input": { + "canonical_json": "{\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"pubkey\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"created_at\":1784347200,\"kind\":31925,\"tags\":[[\"a\",\"31922:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:market\"]],\"content\":\"accepted\",\"sig\":\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}", + "event_kind": 31925, + "family": "calendar_rsvp", + "surface": "raw_nip01_event_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_artifact_invalid_json" + }, + "id": "bud11_authorization_is_rejected", + "input": { + "canonical_json": "{\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"pubkey\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"created_at\":1784347200,\"kind\":24242,\"tags\":[[\"t\",\"upload\"]],\"content\":\"\",\"sig\":\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}", + "event_kind": 24242, + "family": "bud11_authorization", + "surface": "raw_nip01_event_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_artifact_invalid_json" + }, + "id": "ephemeral_event_is_rejected", + "input": { + "canonical_json": "{\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"pubkey\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"created_at\":1784347200,\"kind\":20001,\"tags\":[],\"content\":\"Ephemeral\",\"sig\":\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}", + "event_kind": 20001, + "family": "ephemeral_event", + "surface": "raw_nip01_event_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_artifact_invalid_json" + }, + "id": "trade_event_is_rejected", + "input": { + "canonical_json": "{\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"pubkey\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"created_at\":1784347200,\"kind\":3470,\"tags\":[[\"d\",\"trade-proposal\"]],\"content\":\"\",\"sig\":\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}", + "event_kind": 3470, + "family": "trade", + "surface": "raw_nip01_event_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_artifact_invalid_json" + }, + "id": "order_event_is_rejected", + "input": { + "canonical_json": "{\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"pubkey\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"created_at\":1784347200,\"kind\":3422,\"tags\":[[\"d\",\"order-request\"]],\"content\":\"\",\"sig\":\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}", + "event_kind": 3422, + "family": "commerce_order", + "surface": "raw_nip01_event_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_artifact_invalid_json" + }, + "id": "route_delivery_family_is_rejected", + "input": { + "canonical_json": "{\"product_surface\":\"route_delivery\"}", + "family": "route_delivery", + "surface": "non_event_product_surface" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_artifact_invalid_json" + }, + "id": "group_event_is_rejected", + "input": { + "canonical_json": "{\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"pubkey\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"created_at\":1784347200,\"kind\":9007,\"tags\":[[\"h\",\"victoria-growers\"]],\"content\":\"\",\"sig\":\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}", + "event_kind": 9007, + "family": "group", + "surface": "raw_nip01_event_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + }, + { + "expected": { + "decision": "reject", + "error": "publication_allowlist_artifact_invalid", + "source_error": "publication_artifact_invalid_json" + }, + "id": "operations_event_is_rejected", + "input": { + "canonical_json": "{\"id\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"pubkey\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"created_at\":1784347200,\"kind\":78,\"tags\":[[\"d\",\"internal-operation\"]],\"content\":\"\",\"sig\":\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\"}", + "event_kind": 78, + "family": "operations", + "surface": "raw_nip01_event_json" + }, + "kind": "publication_allowlist.allow_canonical_json.invalid" + } + ] +} diff --git a/crates/event_codec/tests/publication_allowlist.rs b/crates/event_codec/tests/publication_allowlist.rs @@ -0,0 +1,224 @@ +#![cfg(feature = "serde_json")] + +use std::collections::BTreeSet; + +use radroots_event_codec::wire::publication::{ + RadrootsPhase1PublicationArtifact, + allowlist::{ + RadrootsPhase1AllowlistedPublicationArtifact, RadrootsPhase1PublicationAllowlistError, + RadrootsPhase1PublicationLeaf, allow_phase1_publication_artifact, + allow_phase1_publication_canonical_json, + }, +}; +use serde::Deserialize; + +const ALLOWLIST_VECTOR: &str = include_str!("fixtures/phase1_publication_allowlist.v1.json"); + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct VectorSuite { + suite: String, + contract_version: String, + vectors: Vec<VectorCase>, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct VectorCase { + id: String, + kind: String, + input: VectorInput, + expected: VectorExpected, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct VectorInput { + surface: String, + canonical_json: String, + family: Option<String>, + event_kind: Option<u32>, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct VectorExpected { + decision: String, + leaf: Option<String>, + event_kind: Option<u32>, + error: Option<String>, + source_error: Option<String>, +} + +struct Decision { + leaf: Option<RadrootsPhase1PublicationLeaf>, + event_kind: Option<u32>, + error: Option<&'static str>, + source_error: Option<String>, +} + +#[test] +fn publication_allowlist_conformance_vector_executes_every_case() { + let suite: VectorSuite = serde_json::from_str(ALLOWLIST_VECTOR).unwrap(); + assert_eq!(suite.suite, "phase1_publication_allowlist"); + assert_eq!(suite.contract_version, "1.0.0"); + assert_eq!(suite.vectors.len(), 39); + + let mut direct_allowed_leaves = Vec::new(); + let mut canonical_allowed_leaves = Vec::new(); + let mut rejected_families = BTreeSet::new(); + for case in suite.vectors { + assert!(matches!( + case.kind.as_str(), + "publication_allowlist.allow_artifact.valid" + | "publication_allowlist.allow_canonical_json.valid" + | "publication_allowlist.allow_canonical_json.invalid" + )); + let decision = execute_case(&case.kind, &case.input); + match case.expected.decision.as_str() { + "allow" => { + let leaf = decision.leaf.unwrap_or_else(|| panic!("{}", case.id)); + assert_eq!( + leaf.as_str(), + case.expected.leaf.as_deref().unwrap(), + "{}", + case.id + ); + assert_eq!(decision.event_kind, case.expected.event_kind, "{}", case.id); + assert_eq!(decision.error, None, "{}", case.id); + assert_eq!(decision.source_error, None, "{}", case.id); + match case.kind.as_str() { + "publication_allowlist.allow_artifact.valid" => { + direct_allowed_leaves.push(leaf); + } + "publication_allowlist.allow_canonical_json.valid" => { + canonical_allowed_leaves.push(leaf); + } + other => panic!("{} has invalid allow case kind {other}", case.id), + } + } + "reject" => { + assert_eq!(decision.leaf, None, "{}", case.id); + assert_eq!( + decision.error, + case.expected.error.as_deref(), + "{}", + case.id + ); + assert_eq!( + decision.source_error.as_deref(), + case.expected.source_error.as_deref(), + "{}", + case.id + ); + if let Some(family) = case.input.family { + rejected_families.insert(family); + } + } + other => panic!("{} has unknown decision {other}", case.id), + } + } + + let expected_leaves = vec![ + RadrootsPhase1PublicationLeaf::Profile, + RadrootsPhase1PublicationLeaf::Update, + RadrootsPhase1PublicationLeaf::PhotoUpdate, + RadrootsPhase1PublicationLeaf::Ask, + RadrootsPhase1PublicationLeaf::EventDate, + RadrootsPhase1PublicationLeaf::EventTime, + RadrootsPhase1PublicationLeaf::FoodAvailability, + ]; + direct_allowed_leaves.sort(); + canonical_allowed_leaves.sort(); + assert_eq!(direct_allowed_leaves, expected_leaves); + assert_eq!(canonical_allowed_leaves, expected_leaves); + assert_eq!( + rejected_families, + [ + "bud11_authorization", + "calendar_collection", + "calendar_rsvp", + "commerce_order", + "comment", + "deletion_request", + "ephemeral_event", + "generic_calendar", + "generic_nip99", + "generic_root_note", + "group", + "mixed_classified_listing", + "operational_listing", + "operations", + "reply", + "route_delivery", + "trade", + "unsealed_calendar_event", + "unsealed_profile", + ] + .into_iter() + .map(str::to_owned) + .collect() + ); +} + +#[test] +fn publication_allowlist_public_operation_accepts_only_the_sealed_artifact_type() { + type Operation = fn( + RadrootsPhase1PublicationArtifact, + ) -> Result< + RadrootsPhase1AllowlistedPublicationArtifact, + RadrootsPhase1PublicationAllowlistError, + >; + let operation: Operation = allow_phase1_publication_artifact; + let _ = operation; + + type CanonicalOperation = fn( + &[u8], + ) -> Result< + RadrootsPhase1AllowlistedPublicationArtifact, + RadrootsPhase1PublicationAllowlistError, + >; + let canonical_operation: CanonicalOperation = allow_phase1_publication_canonical_json; + let _ = canonical_operation; +} + +fn execute_case(case_kind: &str, input: &VectorInput) -> Decision { + assert!(serde_json::from_str::<serde_json::Value>(&input.canonical_json).is_ok()); + if case_kind == "publication_allowlist.allow_artifact.valid" { + assert_eq!(input.surface, "sealed_artifact_json"); + let artifact = + RadrootsPhase1PublicationArtifact::from_canonical_json(input.canonical_json.as_bytes()) + .unwrap(); + let allowed = allow_phase1_publication_artifact(artifact).unwrap(); + return Decision { + leaf: Some(allowed.leaf()), + event_kind: Some(allowed.artifact().draft().kind()), + error: None, + source_error: None, + }; + } + assert!(matches!( + (case_kind, input.surface.as_str()), + ( + "publication_allowlist.allow_canonical_json.valid", + "canonical_artifact_json" + ) | ( + "publication_allowlist.allow_canonical_json.invalid", + "artifact_candidate_json" | "raw_nip01_event_json" | "non_event_product_surface" + ) + )); + match allow_phase1_publication_canonical_json(input.canonical_json.as_bytes()) { + Ok(allowed) => Decision { + leaf: Some(allowed.leaf()), + event_kind: Some(allowed.artifact().draft().kind()), + error: None, + source_error: None, + }, + Err(error) => Decision { + leaf: None, + event_kind: input.event_kind, + error: Some(error.code()), + source_error: error.source_code().map(str::to_owned), + }, + } +} diff --git a/tools/xtask/src/contract.rs b/tools/xtask/src/contract.rs @@ -6,6 +6,10 @@ mod comment_authority; mod deletion_authority; mod food_availability_projection; mod nip09_reconciliation; +mod phase1_publication_allowlist; +// The former live generator remains compiled for historical unit tests and +// immutable predecessor validation, but the allowlist successor owns writes. +#[allow(dead_code)] mod phase1_publication_artifact; // The raw-source implementation remains compiled for its historical unit tests, while the // publication successor validates its immutable artifacts instead of rebuilding current source. @@ -20,7 +24,11 @@ pub(crate) use food_availability_projection::{ pub(crate) use nip09_reconciliation::{ validate_nip09_reconciliation_manifest, write_nip09_reconciliation_manifest, }; +pub(crate) use phase1_publication_allowlist::{ + validate_phase1_publication_allowlist_manifest, write_phase1_publication_allowlist_manifest, +}; pub(crate) use phase1_publication_artifact::{ + validate_immutable_phase1_publication_artifact_predecessor, validate_phase1_publication_artifact_manifest, write_phase1_publication_artifact_manifest, }; pub(crate) use registry_v7::{ @@ -58,7 +66,8 @@ pub(crate) fn validate_artifact_contracts(workspace_root: &Path) -> Result<(), S validate_food_availability_projection_manifest(workspace_root)?; validate_source_maintenance_manifest(workspace_root)?; validate_raw_source_rebuild_manifest(workspace_root)?; - validate_phase1_publication_artifact_manifest(workspace_root)?; + validate_immutable_phase1_publication_artifact_predecessor(workspace_root)?; + validate_phase1_publication_allowlist_manifest(workspace_root)?; validate_knowledge_contract_manifest(workspace_root) } @@ -108,7 +117,7 @@ const REPLICA_CONTRACT_NAME: &str = "radroots_replica_contract"; const REPLICA_TRANSFER_CONSTANT: &str = "RADROOTS_REPLICA_TRANSFER_VERSION"; const REPLICA_TRANSFER_VERSION: u32 = 2; const VENDORED_WORKSPACE_MEMBER_RELATIVE: &str = "crates/libsqlite3_sys_3_53_3"; -const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 24] = [ +const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 25] = [ ( "contracts/conformance/vectors/blossom/bud11_claims.v1.json", "crates/blossom/tests/fixtures/bud11_claims.v1.json", @@ -198,6 +207,10 @@ const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 24] = [ "crates/event_codec/tests/fixtures/phase1_publication_artifact.v1.json", ), ( + "contracts/conformance/vectors/publication/phase1_allowlist.v1.json", + "crates/event_codec/tests/fixtures/phase1_publication_allowlist.v1.json", + ), + ( "contracts/conformance/vectors/trade/parse_classified_listing_address.v1.json", "crates/trade/tests/fixtures/parse_classified_listing_address.v1.json", ), diff --git a/tools/xtask/src/contract/phase1_publication_allowlist.rs b/tools/xtask/src/contract/phase1_publication_allowlist.rs @@ -0,0 +1,1986 @@ +use super::{ + artifact_bundle::{GeneratedArtifact, read_regular_file, with_artifact_bundle_transaction}, + phase1_publication_artifact::{ + PUBLICATION_SUCCESSOR_SUPERSEDED_PATHS, source_specs, + validate_immutable_phase1_publication_artifact_predecessor_under_lock, + }, +}; +use radroots_event_codec::wire::publication::allowlist::{ + RADROOTS_PHASE1_PUBLICATION_ALLOWLIST_CONTRACT_ID, + RADROOTS_PHASE1_PUBLICATION_ALLOWLIST_REGISTRY_VERSION, + RADROOTS_PHASE1_PUBLICATION_ALLOWLIST_VERSION, +}; +use serde::{Deserialize, Serialize}; +use serde_json::{Value, json}; +use sha2::{Digest, Sha256}; +use std::{collections::BTreeSet, path::Path}; + +const SCHEMA_VERSION: u32 = 1; +const CONTRACT_ID: &str = "radroots_event_codec.phase1_publication_allowlist_v1"; +const AUTHORITY_ID: &str = "phase1_publication_allowlist_v1"; +const HASH_ALGORITHM: &str = "sha256_bytes_v1"; +const OPERATION_ID: &str = "publication_allowlist.allow_artifact"; +const CANONICAL_JSON_OPERATION_ID: &str = "publication_allowlist.allow_canonical_json"; +const WRITE_COMMAND: &str = "cargo xtask contract phase1-publication-allowlist-manifest --write"; + +const MANIFEST_RELATIVE: &str = + "crates/event_codec/contracts/phase1_publication_allowlist_v1.manifest.json"; +const MANIFEST_SCHEMA_RELATIVE: &str = + "crates/event_codec/contracts/phase1_publication_allowlist_v1.manifest.schema.json"; +const MANIFEST_SHA256_RELATIVE: &str = + "crates/event_codec/contracts/phase1_publication_allowlist_v1.manifest.sha256"; +const GENERATED_DESCRIPTOR_RELATIVE: &str = + "crates/event_codec/contracts/phase1_publication_allowlist_v1.descriptor.json"; +const VECTOR_RELATIVE: &str = "contracts/conformance/vectors/publication/phase1_allowlist.v1.json"; +const VECTOR_MIRROR_RELATIVE: &str = + "crates/event_codec/tests/fixtures/phase1_publication_allowlist.v1.json"; +const VECTOR_EXECUTOR_RELATIVE: &str = "crates/event_codec/tests/publication_allowlist.rs"; +const VECTOR_EXECUTOR_TEST: &str = "publication_allowlist_conformance_vector_executes_every_case"; +const OPERATIONS_RELATIVE: &str = "contracts/operations.toml"; +const RELEASE_RELATIVE: &str = "contracts/releases/1.0.0-alpha.1.toml"; +const CHANGELOG_RELATIVE: &str = "CHANGELOG.md"; +const RELEASE_CHANGE_ID: &str = "phase1-publication-allowlist"; +const CHANGELOG_MARKER: &str = "<!-- release-change: phase1-publication-allowlist -->"; +const REGISTRY_RELATIVE: &str = "contracts/event_store/event_contract_registry_v7.inventory.json"; +const REGISTRY_SIDECAR_RELATIVE: &str = + "contracts/event_store/event_contract_registry_v7.inventory.sha256"; +const REGISTRY_BYTE_LENGTH: usize = 158_021; +const REGISTRY_SHA256: &str = "91595544310f865bdef064ee760c227c870417a95b87b3e27278f8da74fdddea"; +const REGISTRY_SIDECAR_BYTE_LENGTH: usize = 65; +const REGISTRY_SIDECAR_SHA256: &str = + "823081e6f423c149865521b185a8517aea35ce4470fa77c165ad7ca5207f036b"; +const VECTOR_AUTHOR: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const VECTOR_ZERO_ID: &str = "0000000000000000000000000000000000000000000000000000000000000000"; +const VECTOR_ZERO_SIGNATURE: &str = "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; +const VECTOR_CREATED_AT: u64 = 1_784_347_200; +const VECTOR_PHOTO_IMETA_FIELDS: &[&str] = &[ + "imeta", + "url https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp", + "x 51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0", + "m image/webp", + "dim 1200x900", + "size 13", + "alt Fresh strawberries", + "fallback https://backup.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp", +]; + +const PREDECESSOR_CONTRACT_ID: &str = "radroots_event_codec.phase1_publication_artifact_v1"; +const PREDECESSOR_ARTIFACTS: &[ImmutableArtifactSpec] = &[ + ImmutableArtifactSpec::new( + "crates/event_codec/contracts/phase1_publication_artifact_v1.manifest.json", + 89_464, + "0776e1d84c9366954047e75cdf12d9acc9a7108260157c3534f22067075a385a", + ), + ImmutableArtifactSpec::new( + "crates/event_codec/contracts/phase1_publication_artifact_v1.manifest.schema.json", + 11_972, + "1d72cee2754e7ac45105d79b1ecf7d44251991be7a18ba106166e962000e8320", + ), + ImmutableArtifactSpec::new( + "crates/event_codec/contracts/phase1_publication_artifact_v1.manifest.sha256", + 65, + "586c0985b502f22241b4d90f2ecb475d43953fc3ddd66d6fbfa3b3eb9cf34444", + ), + ImmutableArtifactSpec::new( + "contracts/conformance/vectors/publication/phase1_artifact.v1.json", + 23_113, + "ec18c687d5b0710a48624ddb620d89157e6b645dbea8bb91c62e3a111d20c622", + ), + ImmutableArtifactSpec::new( + "crates/event_codec/tests/fixtures/phase1_publication_artifact.v1.json", + 23_113, + "ec18c687d5b0710a48624ddb620d89157e6b645dbea8bb91c62e3a111d20c622", + ), + ImmutableArtifactSpec::new( + "crates/event_codec/tests/publication_artifact.rs", + 34_582, + "7a31169eac4217a38cb3ef25eb9213f2f89e11fb17e76ceaf7449b34225e98af", + ), +]; + +const GENERATED_ARTIFACT_PATHS: &[&str] = &[ + MANIFEST_RELATIVE, + MANIFEST_SCHEMA_RELATIVE, + MANIFEST_SHA256_RELATIVE, + GENERATED_DESCRIPTOR_RELATIVE, + VECTOR_MIRROR_RELATIVE, +]; + +const PUBLIC_TYPES: &[&str] = &[ + "RadrootsPhase1PublicationLeaf", + "RadrootsPhase1AllowlistedPublicationArtifact", + "RadrootsPhase1PublicationAllowlistError", +]; + +const LEAVES: &[LeafSpec] = &[ + LeafSpec::new( + "profile", + 0, + "supporting", + "profile.build_authored_draft", + "radroots.profile.metadata.v1", + ), + LeafSpec::new( + "update", + 1, + "root", + "social.update.build_authored_draft", + "radroots.social.update.v1", + ), + LeafSpec::new( + "photo_update", + 1, + "root", + "social.photo_update.build_authored_draft", + "radroots.social.photo_update.v1", + ), + LeafSpec::new( + "ask", + 1, + "root", + "social.ask.build_authored_draft", + "radroots.social.ask.v1", + ), + LeafSpec::new( + "event_date", + 31_922, + "root", + "social.calendar_date_event.build_authored_draft", + "radroots.calendar.date_event.v1", + ), + LeafSpec::new( + "event_time", + 31_923, + "root", + "social.calendar_time_event.build_authored_draft", + "radroots.calendar.time_event.v1", + ), + LeafSpec::new( + "food_availability", + 30_402, + "root", + "food_availability.build_authored_draft", + "radroots.food.availability.v1", + ), +]; + +const DENIED_FAMILIES: &[&str] = &[ + "unsealed_profile", + "unsealed_calendar_event", + "generic_root_note", + "reply", + "comment", + "deletion_request", + "generic_calendar", + "calendar_collection", + "calendar_rsvp", + "bud11_authorization", + "ephemeral_event", + "operational_listing", + "generic_nip99", + "mixed_classified_listing", + "trade", + "commerce_order", + "route_delivery", + "group", + "operations", +]; + +const VALID_VECTOR_SPECS: &[ValidVectorSpec] = &[ + ValidVectorSpec::direct("sealed_profile_is_allowed", 0), + ValidVectorSpec::direct("sealed_update_is_allowed", 1), + ValidVectorSpec::direct("sealed_photo_update_is_allowed", 2), + ValidVectorSpec::direct("sealed_ask_is_allowed", 3), + ValidVectorSpec::direct("sealed_date_event_is_allowed", 4), + ValidVectorSpec::direct("sealed_time_event_is_allowed", 5), + ValidVectorSpec::direct("sealed_food_availability_is_allowed", 6), + ValidVectorSpec::canonical("canonical_profile_is_allowed", 0), + ValidVectorSpec::canonical("canonical_update_is_allowed", 1), + ValidVectorSpec::canonical("canonical_photo_update_is_allowed", 2), + ValidVectorSpec::canonical("canonical_ask_is_allowed", 3), + ValidVectorSpec::canonical("canonical_date_event_is_allowed", 4), + ValidVectorSpec::canonical("canonical_time_event_is_allowed", 5), + ValidVectorSpec::canonical("canonical_food_availability_is_allowed", 6), +]; + +const INVALID_VECTOR_SPECS: &[InvalidVectorSpec] = &[ + InvalidVectorSpec::artifact( + "update_claim_with_ask_marker_is_rejected", + None, + "publication_post_profile_invalid", + ArtifactVectorWitness::UpdateWithAskMarker, + ), + InvalidVectorSpec::artifact( + "photo_claim_with_ask_marker_is_rejected", + None, + "publication_post_profile_invalid", + ArtifactVectorWitness::PhotoUpdateWithAskMarker, + ), + InvalidVectorSpec::artifact( + "ask_claim_without_marker_is_rejected", + None, + "publication_post_profile_invalid", + ArtifactVectorWitness::AskWithoutMarker, + ), + InvalidVectorSpec::artifact( + "update_claim_with_photo_profile_is_rejected", + None, + "publication_post_profile_invalid", + ArtifactVectorWitness::UpdateWithPhotoProfile, + ), + InvalidVectorSpec::artifact( + "typed_date_event_with_generic_shape_is_rejected", + Some("generic_calendar"), + "publication_calendar_profile_invalid", + ArtifactVectorWitness::TypedDateWithGenericShape, + ), + InvalidVectorSpec::artifact( + "typed_time_event_with_generic_shape_is_rejected", + Some("generic_calendar"), + "publication_calendar_profile_invalid", + ArtifactVectorWitness::TypedTimeWithGenericShape, + ), + InvalidVectorSpec::artifact( + "operational_listing_profile_is_rejected", + Some("operational_listing"), + "publication_food_profile_invalid", + ArtifactVectorWitness::OperationalListingProfile, + ), + InvalidVectorSpec::artifact( + "generic_nip99_profile_is_rejected", + Some("generic_nip99"), + "publication_food_profile_invalid", + ArtifactVectorWitness::GenericNip99Profile, + ), + InvalidVectorSpec::artifact( + "mixed_food_and_operational_markers_are_rejected", + Some("mixed_classified_listing"), + "publication_food_profile_invalid", + ArtifactVectorWitness::MixedFoodAndOperationalMarkers, + ), + InvalidVectorSpec::raw( + "unsealed_profile_is_rejected", + "unsealed_profile", + RawEventVectorWitness::Profile, + ), + InvalidVectorSpec::raw( + "generic_root_note_is_rejected", + "generic_root_note", + RawEventVectorWitness::GenericRootNote, + ), + InvalidVectorSpec::raw("reply_is_rejected", "reply", RawEventVectorWitness::Reply), + InvalidVectorSpec::raw( + "comment_is_rejected", + "comment", + RawEventVectorWitness::Comment, + ), + InvalidVectorSpec::raw( + "deletion_request_is_rejected", + "deletion_request", + RawEventVectorWitness::DeletionRequest, + ), + InvalidVectorSpec::raw( + "raw_date_event_is_rejected", + "unsealed_calendar_event", + RawEventVectorWitness::CalendarDate, + ), + InvalidVectorSpec::raw( + "raw_time_event_is_rejected", + "unsealed_calendar_event", + RawEventVectorWitness::CalendarTime, + ), + InvalidVectorSpec::raw( + "calendar_collection_is_rejected", + "calendar_collection", + RawEventVectorWitness::CalendarCollection, + ), + InvalidVectorSpec::raw( + "calendar_rsvp_is_rejected", + "calendar_rsvp", + RawEventVectorWitness::CalendarRsvp, + ), + InvalidVectorSpec::raw( + "bud11_authorization_is_rejected", + "bud11_authorization", + RawEventVectorWitness::Bud11Authorization, + ), + InvalidVectorSpec::raw( + "ephemeral_event_is_rejected", + "ephemeral_event", + RawEventVectorWitness::Ephemeral, + ), + InvalidVectorSpec::raw( + "trade_event_is_rejected", + "trade", + RawEventVectorWitness::Trade, + ), + InvalidVectorSpec::raw( + "order_event_is_rejected", + "commerce_order", + RawEventVectorWitness::CommerceOrder, + ), + InvalidVectorSpec::non_event_route_delivery( + "route_delivery_family_is_rejected", + "route_delivery", + ), + InvalidVectorSpec::raw( + "group_event_is_rejected", + "group", + RawEventVectorWitness::Group, + ), + InvalidVectorSpec::raw( + "operations_event_is_rejected", + "operations", + RawEventVectorWitness::Operations, + ), +]; + +#[derive(Clone, Copy)] +struct ValidVectorSpec { + id: &'static str, + case_kind: &'static str, + surface: &'static str, + leaf_index: usize, +} + +impl ValidVectorSpec { + const fn direct(id: &'static str, leaf_index: usize) -> Self { + Self { + id, + case_kind: "publication_allowlist.allow_artifact.valid", + surface: "sealed_artifact_json", + leaf_index, + } + } + + const fn canonical(id: &'static str, leaf_index: usize) -> Self { + Self { + id, + case_kind: "publication_allowlist.allow_canonical_json.valid", + surface: "canonical_artifact_json", + leaf_index, + } + } +} + +#[derive(Clone, Copy)] +enum ArtifactVectorWitness { + UpdateWithAskMarker, + PhotoUpdateWithAskMarker, + AskWithoutMarker, + UpdateWithPhotoProfile, + TypedDateWithGenericShape, + TypedTimeWithGenericShape, + OperationalListingProfile, + GenericNip99Profile, + MixedFoodAndOperationalMarkers, +} + +#[derive(Clone, Copy)] +enum RawEventVectorWitness { + Profile, + GenericRootNote, + Reply, + Comment, + DeletionRequest, + CalendarDate, + CalendarTime, + CalendarCollection, + CalendarRsvp, + Bud11Authorization, + Ephemeral, + Trade, + CommerceOrder, + Group, + Operations, +} + +impl RawEventVectorWitness { + const fn event_kind(self) -> u32 { + match self { + Self::Profile => 0, + Self::GenericRootNote | Self::Reply => 1, + Self::Comment => 1_111, + Self::DeletionRequest => 5, + Self::CalendarDate => 31_922, + Self::CalendarTime => 31_923, + Self::CalendarCollection => 31_924, + Self::CalendarRsvp => 31_925, + Self::Bud11Authorization => 24_242, + Self::Ephemeral => 20_001, + Self::Trade => 3_470, + Self::CommerceOrder => 3_422, + Self::Group => 9_007, + Self::Operations => 78, + } + } +} + +#[derive(Clone, Copy)] +enum InvalidVectorWitness { + Artifact(ArtifactVectorWitness), + RawEvent(RawEventVectorWitness), + NonEventRouteDelivery, +} + +#[derive(Clone, Copy)] +struct InvalidVectorSpec { + id: &'static str, + family: Option<&'static str>, + source_error: &'static str, + witness: InvalidVectorWitness, +} + +impl InvalidVectorSpec { + const fn artifact( + id: &'static str, + family: Option<&'static str>, + source_error: &'static str, + witness: ArtifactVectorWitness, + ) -> Self { + Self { + id, + family, + source_error, + witness: InvalidVectorWitness::Artifact(witness), + } + } + + const fn raw(id: &'static str, family: &'static str, witness: RawEventVectorWitness) -> Self { + Self { + id, + family: Some(family), + source_error: "publication_artifact_invalid_json", + witness: InvalidVectorWitness::RawEvent(witness), + } + } + + const fn non_event_route_delivery(id: &'static str, family: &'static str) -> Self { + Self { + id, + family: Some(family), + source_error: "publication_artifact_invalid_json", + witness: InvalidVectorWitness::NonEventRouteDelivery, + } + } + + const fn surface(self) -> &'static str { + match self.witness { + InvalidVectorWitness::Artifact(_) => "artifact_candidate_json", + InvalidVectorWitness::RawEvent(_) => "raw_nip01_event_json", + InvalidVectorWitness::NonEventRouteDelivery => "non_event_product_surface", + } + } +} + +#[derive(Clone, Copy)] +struct ImmutableArtifactSpec { + relative: &'static str, + byte_length: usize, + sha256: &'static str, +} + +impl ImmutableArtifactSpec { + const fn new(relative: &'static str, byte_length: usize, sha256: &'static str) -> Self { + Self { + relative, + byte_length, + sha256, + } + } +} + +#[derive(Clone, Copy)] +struct LeafSpec { + leaf: &'static str, + kind: u32, + publication_role: &'static str, + authored_operation_id: &'static str, + event_contract_id: &'static str, +} + +impl LeafSpec { + const fn new( + leaf: &'static str, + kind: u32, + publication_role: &'static str, + authored_operation_id: &'static str, + event_contract_id: &'static str, + ) -> Self { + Self { + leaf, + kind, + publication_role, + authored_operation_id, + event_contract_id, + } + } +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct FileDescriptor { + path: String, + byte_length: u64, + sha256: String, + hash_algorithm: String, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct SourceFileDescriptor { + role: String, + path: String, + byte_length: u64, + sha256: String, + hash_algorithm: String, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct PredecessorDescriptor { + contract_id: String, + immutable_artifacts: Vec<FileDescriptor>, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct RegistryDescriptor { + version: u32, + inventory: FileDescriptor, + sidecar: FileDescriptor, + evolution: String, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct LeafDescriptor { + leaf: String, + kind: u32, + publication_role: String, + authored_operation_id: String, + event_contract_id: String, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct AllowlistDescriptor { + version: u32, + contract_id: String, + operation_id: String, + canonical_json_operation_id: String, + input_type: String, + output_type: String, + allowed_leaves: Vec<LeafDescriptor>, + kind_one_precedence: Vec<String>, + event_policy: String, + classified_listing_policy: String, + denied_families: Vec<String>, + granted_capability: String, + excluded_capabilities: Vec<String>, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct ResultVectorDescriptor { + canonical_path: String, + mirror_path: String, + byte_length: u64, + sha256: String, + hash_algorithm: String, + executor_path: String, + executor_test: String, + valid_case_ids: Vec<String>, + invalid_case_ids: Vec<String>, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct ReleaseDescriptor { + record_path: String, + change_id: String, + changelog_path: String, + changelog_marker: String, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct AllowlistManifest { + schema_version: u32, + contract_id: String, + authority_id: String, + manifest_schema: FileDescriptor, + predecessor: PredecessorDescriptor, + event_contract_registry: RegistryDescriptor, + allowlist: AllowlistDescriptor, + predecessor_source_supersessions: Vec<String>, + source_files: Vec<SourceFileDescriptor>, + result_vector: ResultVectorDescriptor, + release: ReleaseDescriptor, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct VectorSuite { + suite: String, + contract_version: String, + vectors: Vec<VectorCase>, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct VectorCase { + id: String, + kind: String, + input: Value, + expected: Value, +} + +struct ValidatedVector { + bytes: Vec<u8>, + valid_case_ids: Vec<String>, + invalid_case_ids: Vec<String>, +} + +pub(crate) fn write_phase1_publication_allowlist_manifest( + workspace_root: &Path, +) -> Result<(), String> { + with_artifact_bundle_transaction(workspace_root, |transaction| { + transaction.write(expected_artifacts(workspace_root)?)?; + validate_manifest_under_lock(workspace_root) + }) +} + +pub(crate) fn validate_phase1_publication_allowlist_manifest( + workspace_root: &Path, +) -> Result<(), String> { + with_artifact_bundle_transaction(workspace_root, |_| { + validate_manifest_under_lock(workspace_root) + }) +} + +fn validate_manifest_under_lock(workspace_root: &Path) -> Result<(), String> { + validate_immutable_phase1_publication_artifact_predecessor_under_lock(workspace_root)?; + for artifact in expected_artifacts(workspace_root)? { + let actual = read_regular_file(workspace_root, artifact.relative)?; + if actual != artifact.contents { + return Err(format!( + "generated Phase 1 publication allowlist contract {} is stale; run {WRITE_COMMAND}", + artifact.relative + )); + } + } + + let bytes = read_regular_file(workspace_root, MANIFEST_RELATIVE)?; + let manifest: AllowlistManifest = serde_json::from_slice(&bytes) + .map_err(|error| format!("parse {MANIFEST_RELATIVE}: {error}"))?; + validate_canonical_json(MANIFEST_RELATIVE, &bytes, &manifest)?; + validate_manifest_shape(&manifest)?; + + let schema_bytes = read_regular_file(workspace_root, MANIFEST_SCHEMA_RELATIVE)?; + let schema: Value = serde_json::from_slice(&schema_bytes) + .map_err(|error| format!("parse {MANIFEST_SCHEMA_RELATIVE}: {error}"))?; + validate_canonical_json(MANIFEST_SCHEMA_RELATIVE, &schema_bytes, &schema)?; + let instance = serde_json::to_value(&manifest) + .map_err(|error| format!("serialize {MANIFEST_RELATIVE}: {error}"))?; + validate_json_schema(&schema, &instance)?; + + let sidecar = read_regular_file(workspace_root, MANIFEST_SHA256_RELATIVE)?; + if sidecar != format!("{}\n", sha256_hex(&bytes)).as_bytes() { + return Err(format!( + "{MANIFEST_SHA256_RELATIVE} must authenticate the exact allowlist manifest bytes" + )); + } + Ok(()) +} + +fn expected_artifacts(workspace_root: &Path) -> Result<Vec<GeneratedArtifact>, String> { + validate_immutable_phase1_publication_artifact_predecessor_under_lock(workspace_root)?; + validate_source_contract(workspace_root)?; + let schema_bytes = canonical_json_bytes(&manifest_schema())?; + let manifest = describe_manifest(workspace_root, &schema_bytes)?; + let manifest_bytes = canonical_json_bytes(&manifest)?; + let sidecar_bytes = format!("{}\n", sha256_hex(&manifest_bytes)).into_bytes(); + let descriptor_bytes = canonical_json_bytes(&json!({ + "schema_version": SCHEMA_VERSION, + "contract_id": CONTRACT_ID, + "manifest": descriptor_for_bytes(MANIFEST_RELATIVE, &manifest_bytes)?, + "manifest_schema": descriptor_for_bytes(MANIFEST_SCHEMA_RELATIVE, &schema_bytes)?, + "manifest_sidecar": descriptor_for_bytes(MANIFEST_SHA256_RELATIVE, &sidecar_bytes)?, + "predecessor_manifest_sha256": PREDECESSOR_ARTIFACTS[0].sha256, + "event_contract_registry_v7_sha256": REGISTRY_SHA256, + }))?; + let vector_bytes = read_regular_file(workspace_root, VECTOR_RELATIVE)?; + Ok(vec![ + GeneratedArtifact { + relative: MANIFEST_RELATIVE, + contents: manifest_bytes, + }, + GeneratedArtifact { + relative: MANIFEST_SCHEMA_RELATIVE, + contents: schema_bytes, + }, + GeneratedArtifact { + relative: MANIFEST_SHA256_RELATIVE, + contents: sidecar_bytes, + }, + GeneratedArtifact { + relative: GENERATED_DESCRIPTOR_RELATIVE, + contents: descriptor_bytes, + }, + GeneratedArtifact { + relative: VECTOR_MIRROR_RELATIVE, + contents: vector_bytes, + }, + ]) +} + +fn describe_manifest( + workspace_root: &Path, + schema_bytes: &[u8], +) -> Result<AllowlistManifest, String> { + let source_files = successor_source_specs(workspace_root)? + .into_iter() + .map(|(role, path)| source_descriptor(workspace_root, &role, &path)) + .collect::<Result<Vec<_>, _>>()?; + let vector = validate_vector(workspace_root)?; + Ok(AllowlistManifest { + schema_version: SCHEMA_VERSION, + contract_id: CONTRACT_ID.to_owned(), + authority_id: AUTHORITY_ID.to_owned(), + manifest_schema: descriptor_for_bytes(MANIFEST_SCHEMA_RELATIVE, schema_bytes)?, + predecessor: PredecessorDescriptor { + contract_id: PREDECESSOR_CONTRACT_ID.to_owned(), + immutable_artifacts: PREDECESSOR_ARTIFACTS + .iter() + .map(|spec| FileDescriptor { + path: spec.relative.to_owned(), + byte_length: spec.byte_length as u64, + sha256: spec.sha256.to_owned(), + hash_algorithm: HASH_ALGORITHM.to_owned(), + }) + .collect(), + }, + event_contract_registry: RegistryDescriptor { + version: RADROOTS_PHASE1_PUBLICATION_ALLOWLIST_REGISTRY_VERSION, + inventory: descriptor_for_file(workspace_root, REGISTRY_RELATIVE)?, + sidecar: descriptor_for_file(workspace_root, REGISTRY_SIDECAR_RELATIVE)?, + evolution: "immutable_registry_v7_plus_additive_phase1_publication_allowlist_v1" + .to_owned(), + }, + allowlist: expected_allowlist_descriptor(), + predecessor_source_supersessions: PUBLICATION_SUCCESSOR_SUPERSEDED_PATHS + .iter() + .map(|path| (*path).to_owned()) + .collect(), + source_files, + result_vector: ResultVectorDescriptor { + canonical_path: VECTOR_RELATIVE.to_owned(), + mirror_path: VECTOR_MIRROR_RELATIVE.to_owned(), + byte_length: vector.bytes.len() as u64, + sha256: sha256_hex(&vector.bytes), + hash_algorithm: HASH_ALGORITHM.to_owned(), + executor_path: VECTOR_EXECUTOR_RELATIVE.to_owned(), + executor_test: VECTOR_EXECUTOR_TEST.to_owned(), + valid_case_ids: vector.valid_case_ids, + invalid_case_ids: vector.invalid_case_ids, + }, + release: ReleaseDescriptor { + record_path: RELEASE_RELATIVE.to_owned(), + change_id: RELEASE_CHANGE_ID.to_owned(), + changelog_path: CHANGELOG_RELATIVE.to_owned(), + changelog_marker: CHANGELOG_MARKER.to_owned(), + }, + }) +} + +fn expected_allowlist_descriptor() -> AllowlistDescriptor { + AllowlistDescriptor { + version: RADROOTS_PHASE1_PUBLICATION_ALLOWLIST_VERSION, + contract_id: RADROOTS_PHASE1_PUBLICATION_ALLOWLIST_CONTRACT_ID.to_owned(), + operation_id: OPERATION_ID.to_owned(), + canonical_json_operation_id: CANONICAL_JSON_OPERATION_ID.to_owned(), + input_type: "RadrootsPhase1PublicationArtifact".to_owned(), + output_type: "RadrootsPhase1AllowlistedPublicationArtifact".to_owned(), + allowed_leaves: LEAVES + .iter() + .map(|leaf| LeafDescriptor { + leaf: leaf.leaf.to_owned(), + kind: leaf.kind, + publication_role: leaf.publication_role.to_owned(), + authored_operation_id: leaf.authored_operation_id.to_owned(), + event_contract_id: leaf.event_contract_id.to_owned(), + }) + .collect(), + kind_one_precedence: ["ask", "photo_update", "update"] + .into_iter() + .map(str::to_owned) + .collect(), + event_policy: "strict_typed_nip52_date_or_time_artifact_only_v1".to_owned(), + classified_listing_policy: "raw_marker_partition_before_focused_food_profile_validation_v1" + .to_owned(), + denied_families: DENIED_FAMILIES + .iter() + .map(|family| (*family).to_owned()) + .collect(), + granted_capability: "phase1_durable_publication_lane_entry_only_v1".to_owned(), + excluded_capabilities: [ + "signing", + "media_upload", + "media_retrieval", + "media_readiness", + "relay_transport", + "product_entitlement", + "inbound_registry_admission", + ] + .into_iter() + .map(str::to_owned) + .collect(), + } +} + +fn validate_source_contract(workspace_root: &Path) -> Result<(), String> { + validate_registry_identity(workspace_root)?; + validate_operations_authority(workspace_root)?; + validate_release_authority(workspace_root)?; + validate_vector(workspace_root)?; + + let paths = successor_source_specs(workspace_root)? + .into_iter() + .map(|(_, path)| path) + .collect::<BTreeSet<_>>(); + for path in PUBLICATION_SUCCESSOR_SUPERSEDED_PATHS { + if !paths.contains(*path) { + return Err(format!( + "allowlist successor does not bind superseded predecessor source {path}" + )); + } + } + for path in GENERATED_ARTIFACT_PATHS { + if paths.contains(*path) { + return Err(format!( + "allowlist source inventory recursively includes generated artifact {path}" + )); + } + } + Ok(()) +} + +fn validate_registry_identity(workspace_root: &Path) -> Result<(), String> { + for (path, expected_len, expected_sha) in [ + (REGISTRY_RELATIVE, REGISTRY_BYTE_LENGTH, REGISTRY_SHA256), + ( + REGISTRY_SIDECAR_RELATIVE, + REGISTRY_SIDECAR_BYTE_LENGTH, + REGISTRY_SIDECAR_SHA256, + ), + ] { + let bytes = read_regular_file(workspace_root, path)?; + if bytes.len() != expected_len || sha256_hex(&bytes) != expected_sha { + return Err(format!( + "immutable event-contract registry-v7 artifact {path} drifted" + )); + } + } + let inventory = read_regular_file(workspace_root, REGISTRY_RELATIVE)?; + let value: Value = serde_json::from_slice(&inventory) + .map_err(|error| format!("parse {REGISTRY_RELATIVE}: {error}"))?; + if value + .get("event_contract_registry_version") + .and_then(Value::as_u64) + != Some(7) + { + return Err("event-contract registry-v7 identity drifted".to_owned()); + } + let sidecar = read_regular_file(workspace_root, REGISTRY_SIDECAR_RELATIVE)?; + if sidecar != format!("{}\n", sha256_hex(&inventory)).as_bytes() { + return Err( + "event-contract registry-v7 sidecar does not authenticate inventory".to_owned(), + ); + } + Ok(()) +} + +fn validate_operations_authority(workspace_root: &Path) -> Result<(), String> { + let manifest = parse_toml(workspace_root, OPERATIONS_RELATIVE)?; + let public_types = toml_string_array( + "shared_types.public", + manifest + .get("shared_types") + .and_then(|value| value.get("public")), + )?; + for required in PUBLIC_TYPES { + if public_types + .iter() + .filter(|candidate| candidate.as_str() == *required) + .count() + != 1 + { + return Err(format!( + "shared public types must contain {required} exactly once" + )); + } + } + let operations = manifest + .get("operations") + .and_then(toml::Value::as_table) + .ok_or_else(|| "operations.toml has no operations table".to_owned())?; + for (key, operation_id, input) in [ + ( + "phase1_publication_allowlist_allow_artifact", + OPERATION_ID, + "RadrootsPhase1PublicationArtifact", + ), + ( + "phase1_publication_allowlist_allow_canonical_json", + CANONICAL_JSON_OPERATION_ID, + "Bytes", + ), + ] { + let operation = operations + .get(key) + .and_then(toml::Value::as_table) + .ok_or_else(|| format!("operations.toml is missing {key}"))?; + let case_kinds: &[&str] = if operation_id == OPERATION_ID { + &["publication_allowlist.allow_artifact.valid"] + } else { + &[ + "publication_allowlist.allow_canonical_json.valid", + "publication_allowlist.allow_canonical_json.invalid", + ] + }; + validate_allowlist_operation(operation, operation_id, input, case_kinds)?; + } + Ok(()) +} + +fn validate_allowlist_operation( + operation: &toml::map::Map<String, toml::Value>, + operation_id: &str, + input: &str, + case_kinds: &[&str], +) -> Result<(), String> { + for (field, expected) in [ + ("domain", "publication"), + ("id", operation_id), + ("stability", "beta"), + ("error_class", "validation_error"), + ("signing", "none"), + ("transport", "none"), + ] { + if operation.get(field).and_then(toml::Value::as_str) != Some(expected) { + return Err(format!( + "publication allowlist operation {field} must be {expected}" + )); + } + } + if operation + .get("deterministic") + .and_then(toml::Value::as_bool) + != Some(true) + || toml_string_array("allowlist inputs", operation.get("inputs"))? != vec![input.to_owned()] + || toml_string_array("allowlist outputs", operation.get("outputs"))? + != vec!["RadrootsPhase1AllowlistedPublicationArtifact".to_owned()] + { + return Err("publication allowlist signature or determinism drifted".to_owned()); + } + let implementation = operation + .get("implementation") + .and_then(toml::Value::as_table) + .ok_or_else(|| "publication allowlist implementation is missing".to_owned())?; + let modules = toml_string_array( + "allowlist implementation modules", + implementation.get("rust_modules"), + )?; + for required in [ + "crates/event_codec/src/wire/publication.rs", + "crates/event_codec/src/wire/publication/allowlist.rs", + "crates/event_codec/src/post/inbound/registry_v7.rs", + "crates/event_codec/src/food_availability/inbound/registry_v7.rs", + ] { + if !modules.iter().any(|module| module == required) { + return Err(format!( + "publication allowlist implementation is missing {required}" + )); + } + } + let conformance = operation + .get("conformance") + .and_then(toml::Value::as_table) + .ok_or_else(|| "publication allowlist conformance is missing".to_owned())?; + if conformance.get("vector").and_then(toml::Value::as_str) != Some(VECTOR_RELATIVE) + || toml_string_array("allowlist case kinds", conformance.get("case_kinds"))? + != case_kinds + .iter() + .map(|case_kind| (*case_kind).to_owned()) + .collect::<Vec<_>>() + { + return Err("publication allowlist conformance authority drifted".to_owned()); + } + Ok(()) +} + +fn validate_release_authority(workspace_root: &Path) -> Result<(), String> { + let release = parse_toml(workspace_root, RELEASE_RELATIVE)?; + let changes = release + .get("changes") + .and_then(toml::Value::as_array) + .ok_or_else(|| format!("{RELEASE_RELATIVE} has no changes"))?; + let matches = changes + .iter() + .filter(|change| change.get("id").and_then(toml::Value::as_str) == Some(RELEASE_CHANGE_ID)) + .collect::<Vec<_>>(); + if matches.len() != 1 + || matches[0] + .get("classification") + .and_then(toml::Value::as_str) + != Some("feature") + { + return Err(format!( + "{RELEASE_RELATIVE} must contain one feature change {RELEASE_CHANGE_ID}" + )); + } + let impacts = toml_string_array("allowlist semver impacts", matches[0].get("semver_impacts"))?; + for required in [ + "add_exported_type", + "add_exported_function", + "add_exported_constant", + "add_conformance_vector", + ] { + if !impacts.iter().any(|impact| impact == required) { + return Err(format!("allowlist release change is missing {required}")); + } + } + let changelog = String::from_utf8(read_regular_file(workspace_root, CHANGELOG_RELATIVE)?) + .map_err(|error| format!("{CHANGELOG_RELATIVE} must be UTF-8: {error}"))?; + if changelog.matches(CHANGELOG_MARKER).count() != 1 { + return Err(format!( + "{CHANGELOG_RELATIVE} must contain {CHANGELOG_MARKER} exactly once" + )); + } + Ok(()) +} + +fn validate_vector(workspace_root: &Path) -> Result<ValidatedVector, String> { + let bytes = read_regular_file(workspace_root, VECTOR_RELATIVE)?; + let suite: VectorSuite = serde_json::from_slice(&bytes) + .map_err(|error| format!("parse {VECTOR_RELATIVE}: {error}"))?; + let canonical = canonical_json_bytes( + &serde_json::from_slice::<Value>(&bytes) + .map_err(|error| format!("parse {VECTOR_RELATIVE} as JSON value: {error}"))?, + )?; + if canonical != bytes { + return Err(format!("{VECTOR_RELATIVE} must be canonical pretty JSON")); + } + if suite.suite != "phase1_publication_allowlist" || suite.contract_version != "1.0.0" { + return Err(format!("{VECTOR_RELATIVE} identity drifted")); + } + let expected_ids = VALID_VECTOR_SPECS + .iter() + .map(|spec| spec.id) + .chain(INVALID_VECTOR_SPECS.iter().map(|spec| spec.id)) + .collect::<Vec<_>>(); + let actual_ids = suite + .vectors + .iter() + .map(|vector| vector.id.as_str()) + .collect::<Vec<_>>(); + if actual_ids != expected_ids { + return Err(format!("{VECTOR_RELATIVE} case inventory or order drifted")); + } + if actual_ids.iter().copied().collect::<BTreeSet<_>>().len() != actual_ids.len() { + return Err(format!("{VECTOR_RELATIVE} case ids must be unique")); + } + let mut denied_families = BTreeSet::new(); + for (index, vector) in suite.vectors.iter().enumerate() { + let input = vector + .input + .as_object() + .ok_or_else(|| format!("{} input must be an object", vector.id))?; + let expected = vector + .expected + .as_object() + .ok_or_else(|| format!("{} expected must be an object", vector.id))?; + let canonical_json = input + .get("canonical_json") + .and_then(Value::as_str) + .ok_or_else(|| format!("{} must carry exact canonical_json", vector.id))?; + let candidate: Value = serde_json::from_str(canonical_json) + .map_err(|error| format!("{} canonical_json is invalid: {error}", vector.id))?; + if let Some(spec) = VALID_VECTOR_SPECS.get(index).copied() { + let leaf = LEAVES[spec.leaf_index]; + let input_keys = input.keys().map(String::as_str).collect::<BTreeSet<_>>(); + let expected_input_keys = ["canonical_json", "surface"] + .into_iter() + .collect::<BTreeSet<_>>(); + if input.len() != 2 + || input_keys != expected_input_keys + || vector.kind != spec.case_kind + || input.get("surface").and_then(Value::as_str) != Some(spec.surface) + || expected.len() != 3 + || expected.get("decision").and_then(Value::as_str) != Some("allow") + || expected.get("leaf").and_then(Value::as_str) != Some(leaf.leaf) + || expected.get("event_kind").and_then(Value::as_u64) != Some(u64::from(leaf.kind)) + || candidate.get("semantic_variant").and_then(Value::as_str) != Some(leaf.leaf) + || candidate + .get("authored_operation_id") + .and_then(Value::as_str) + != Some(leaf.authored_operation_id) + || candidate.get("event_contract_id").and_then(Value::as_str) + != Some(leaf.event_contract_id) + || candidate.pointer("/draft/kind").and_then(Value::as_u64) + != Some(u64::from(leaf.kind)) + { + return Err(format!("{} allowed leaf descriptor drifted", vector.id)); + } + validate_artifact_envelope_keys(&vector.id, &candidate)?; + if spec.case_kind == "publication_allowlist.allow_canonical_json.valid" + && canonical_json + != suite.vectors[spec.leaf_index] + .input + .get("canonical_json") + .and_then(Value::as_str) + .ok_or_else(|| { + format!("{} direct-operation counterpart is missing", vector.id) + })? + { + return Err(format!( + "{} must reuse the exact direct-operation artifact bytes", + vector.id + )); + } + } else { + let spec = INVALID_VECTOR_SPECS[index - VALID_VECTOR_SPECS.len()]; + let family = input.get("family").and_then(Value::as_str); + if vector.kind != "publication_allowlist.allow_canonical_json.invalid" + || input.get("surface").and_then(Value::as_str) != Some(spec.surface()) + || family != spec.family + || expected.len() != 3 + || expected.get("decision").and_then(Value::as_str) != Some("reject") + || expected.get("error").and_then(Value::as_str) + != Some("publication_allowlist_artifact_invalid") + || expected.get("source_error").and_then(Value::as_str) != Some(spec.source_error) + { + return Err(format!("{} rejection authority drifted", vector.id)); + } + if let Some(family) = family { + denied_families.insert(family); + } + validate_invalid_vector_evidence(&vector.id, spec, input, &candidate)?; + } + } + let expected_denied = DENIED_FAMILIES.iter().copied().collect::<BTreeSet<_>>(); + if denied_families != expected_denied { + return Err(format!( + "{VECTOR_RELATIVE} denied-family evidence does not match the declared taxonomy" + )); + } + Ok(ValidatedVector { + bytes, + valid_case_ids: VALID_VECTOR_SPECS + .iter() + .map(|spec| spec.id.to_owned()) + .collect(), + invalid_case_ids: INVALID_VECTOR_SPECS + .iter() + .map(|spec| spec.id.to_owned()) + .collect(), + }) +} + +fn validate_invalid_vector_evidence( + case_id: &str, + spec: InvalidVectorSpec, + input: &serde_json::Map<String, Value>, + candidate: &Value, +) -> Result<(), String> { + match spec.witness { + InvalidVectorWitness::Artifact(witness) => { + let expected_input_len = 2 + usize::from(spec.family.is_some()); + if input.len() != expected_input_len || input.get("event_kind").is_some() { + return Err(format!("{case_id} artifact input evidence drifted")); + } + validate_artifact_envelope_keys(case_id, candidate)?; + validate_artifact_vector_witness(case_id, witness, candidate) + } + InvalidVectorWitness::RawEvent(witness) => { + let event_kind = input + .get("event_kind") + .and_then(Value::as_u64) + .ok_or_else(|| format!("{case_id} raw event kind is missing"))?; + if input.len() != 4 || event_kind != u64::from(witness.event_kind()) { + return Err(format!("{case_id} raw event input evidence drifted")); + } + validate_raw_event_vector_witness(case_id, witness, candidate) + } + InvalidVectorWitness::NonEventRouteDelivery => { + let input_keys = input.keys().map(String::as_str).collect::<BTreeSet<_>>(); + let expected_input_keys = ["canonical_json", "family", "surface"] + .into_iter() + .collect::<BTreeSet<_>>(); + if input.len() != 3 + || input_keys != expected_input_keys + || candidate != &json!({"product_surface": "route_delivery"}) + { + return Err(format!( + "{case_id} must witness route/delivery as an exact non-event product surface" + )); + } + Ok(()) + } + } +} + +fn validate_artifact_envelope_keys(case_id: &str, candidate: &Value) -> Result<(), String> { + let candidate = candidate + .as_object() + .ok_or_else(|| format!("{case_id} artifact candidate must be an object"))?; + let actual_keys = candidate + .keys() + .map(String::as_str) + .collect::<BTreeSet<_>>(); + let expected_keys = [ + "schema_version", + "semantic_variant", + "authored_operation_id", + "event_contract_id", + "expected_author", + "draft", + "expected_event_id", + "media_references", + "artifact_digest", + ] + .into_iter() + .collect::<BTreeSet<_>>(); + if actual_keys != expected_keys { + return Err(format!("{case_id} artifact envelope evidence drifted")); + } + Ok(()) +} + +struct ArtifactWitnessDescriptor { + semantic_variant: &'static str, + authored_operation_id: &'static str, + event_contract_id: &'static str, + event_kind: u32, + tag_names: &'static [&'static str], + content: &'static str, + media_reference_count: usize, +} + +fn artifact_witness_descriptor(witness: ArtifactVectorWitness) -> ArtifactWitnessDescriptor { + match witness { + ArtifactVectorWitness::UpdateWithAskMarker => ArtifactWitnessDescriptor { + semantic_variant: "update", + authored_operation_id: "social.update.build_authored_draft", + event_contract_id: "radroots.social.update.v1", + event_kind: 1, + tag_names: &["t"], + content: "Carrots harvested today", + media_reference_count: 0, + }, + ArtifactVectorWitness::PhotoUpdateWithAskMarker => ArtifactWitnessDescriptor { + semantic_variant: "photo_update", + authored_operation_id: "social.photo_update.build_authored_draft", + event_contract_id: "radroots.social.photo_update.v1", + event_kind: 1, + tag_names: &["t", "imeta"], + content: "Strawberries at the farm stand https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp", + media_reference_count: 2, + }, + ArtifactVectorWitness::AskWithoutMarker => ArtifactWitnessDescriptor { + semantic_variant: "ask", + authored_operation_id: "social.ask.build_authored_draft", + event_contract_id: "radroots.social.ask.v1", + event_kind: 1, + tag_names: &["imeta"], + content: "When will strawberries be ready? https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp", + media_reference_count: 2, + }, + ArtifactVectorWitness::UpdateWithPhotoProfile => ArtifactWitnessDescriptor { + semantic_variant: "update", + authored_operation_id: "social.update.build_authored_draft", + event_contract_id: "radroots.social.update.v1", + event_kind: 1, + tag_names: &["imeta"], + content: "Strawberries at the farm stand https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp", + media_reference_count: 2, + }, + ArtifactVectorWitness::TypedDateWithGenericShape => ArtifactWitnessDescriptor { + semantic_variant: "event_date", + authored_operation_id: "social.calendar_date_event.build_authored_draft", + event_contract_id: "radroots.calendar.date_event.v1", + event_kind: 31_922, + tag_names: &["d", "title"], + content: "Saturday market in Victoria", + media_reference_count: 0, + }, + ArtifactVectorWitness::TypedTimeWithGenericShape => ArtifactWitnessDescriptor { + semantic_variant: "event_time", + authored_operation_id: "social.calendar_time_event.build_authored_draft", + event_contract_id: "radroots.calendar.time_event.v1", + event_kind: 31_923, + tag_names: &["d", "title"], + content: "A one-hour farm tour", + media_reference_count: 0, + }, + ArtifactVectorWitness::OperationalListingProfile => ArtifactWitnessDescriptor { + semantic_variant: "food_availability", + authored_operation_id: "food_availability.build_authored_draft", + event_contract_id: "radroots.food.availability.v1", + event_kind: 30_402, + tag_names: &[ + "d", + "title", + "summary", + "published_at", + "location", + "price", + "status", + "image", + "radroots:price", + ], + content: "Fresh Nantes carrots available this week.", + media_reference_count: 1, + }, + ArtifactVectorWitness::GenericNip99Profile => ArtifactWitnessDescriptor { + semantic_variant: "food_availability", + authored_operation_id: "food_availability.build_authored_draft", + event_contract_id: "radroots.food.availability.v1", + event_kind: 30_402, + tag_names: &[ + "d", + "title", + "summary", + "published_at", + "location", + "price", + "status", + "image", + ], + content: "Fresh Nantes carrots available this week.", + media_reference_count: 1, + }, + ArtifactVectorWitness::MixedFoodAndOperationalMarkers => ArtifactWitnessDescriptor { + semantic_variant: "food_availability", + authored_operation_id: "food_availability.build_authored_draft", + event_contract_id: "radroots.food.availability.v1", + event_kind: 30_402, + tag_names: &[ + "d", + "title", + "summary", + "published_at", + "location", + "price", + "radroots:price_unit", + "radroots:quantity", + "status", + "image", + "radroots:price", + ], + content: "Fresh Nantes carrots available this week.", + media_reference_count: 1, + }, + } +} + +fn validate_artifact_vector_witness( + case_id: &str, + witness: ArtifactVectorWitness, + candidate: &Value, +) -> Result<(), String> { + let descriptor = artifact_witness_descriptor(witness); + let tags = candidate + .pointer("/draft/tags") + .and_then(Value::as_array) + .ok_or_else(|| format!("{case_id} artifact tags are missing"))?; + let media_references = candidate + .get("media_references") + .and_then(Value::as_array) + .ok_or_else(|| format!("{case_id} media references are missing"))?; + if candidate.get("schema_version").and_then(Value::as_u64) != Some(1) + || candidate.get("semantic_variant").and_then(Value::as_str) + != Some(descriptor.semantic_variant) + || candidate + .get("authored_operation_id") + .and_then(Value::as_str) + != Some(descriptor.authored_operation_id) + || candidate.get("event_contract_id").and_then(Value::as_str) + != Some(descriptor.event_contract_id) + || candidate.get("expected_author").and_then(Value::as_str) != Some(VECTOR_AUTHOR) + || candidate.pointer("/draft/kind").and_then(Value::as_u64) + != Some(u64::from(descriptor.event_kind)) + || candidate + .pointer("/draft/created_at") + .and_then(Value::as_u64) + != Some(VECTOR_CREATED_AT) + || candidate.pointer("/draft/content").and_then(Value::as_str) != Some(descriptor.content) + || media_references.len() != descriptor.media_reference_count + || candidate.get("artifact_digest").and_then(Value::as_str) != Some(VECTOR_ZERO_ID) + { + return Err(format!("{case_id} claimed artifact profile drifted")); + } + validate_exact_tag_names(case_id, tags, descriptor.tag_names)?; + + match witness { + ArtifactVectorWitness::UpdateWithAskMarker => { + require_exact_tag(case_id, tags, &["t", "radroots-ask"])?; + } + ArtifactVectorWitness::PhotoUpdateWithAskMarker => { + require_exact_tag(case_id, tags, &["t", "radroots-ask"])?; + require_exact_tag(case_id, tags, VECTOR_PHOTO_IMETA_FIELDS)?; + } + ArtifactVectorWitness::AskWithoutMarker | ArtifactVectorWitness::UpdateWithPhotoProfile => { + forbid_tag_name(case_id, tags, "t")?; + require_exact_tag(case_id, tags, VECTOR_PHOTO_IMETA_FIELDS)?; + } + ArtifactVectorWitness::TypedDateWithGenericShape + | ArtifactVectorWitness::TypedTimeWithGenericShape => { + require_exact_tag(case_id, tags, &["d", "generic-calendar"])?; + require_exact_tag(case_id, tags, &["title", "Generic calendar event"])?; + } + ArtifactVectorWitness::OperationalListingProfile => { + require_exact_tag(case_id, tags, &["radroots:price", "3", "CAD"])?; + forbid_tag_name(case_id, tags, "radroots:price_unit")?; + forbid_tag_name(case_id, tags, "radroots:quantity")?; + } + ArtifactVectorWitness::GenericNip99Profile => { + forbid_tag_name(case_id, tags, "radroots:price")?; + forbid_tag_name(case_id, tags, "radroots:price_unit")?; + forbid_tag_name(case_id, tags, "radroots:quantity")?; + } + ArtifactVectorWitness::MixedFoodAndOperationalMarkers => { + require_exact_tag(case_id, tags, &["radroots:price", "3", "CAD"])?; + require_exact_tag(case_id, tags, &["radroots:price_unit", "lb"])?; + require_exact_tag(case_id, tags, &["radroots:quantity", "24", "lb"])?; + } + } + Ok(()) +} + +fn validate_raw_event_vector_witness( + case_id: &str, + witness: RawEventVectorWitness, + candidate: &Value, +) -> Result<(), String> { + let candidate = candidate + .as_object() + .ok_or_else(|| format!("{case_id} raw event must be an object"))?; + let actual_keys = candidate + .keys() + .map(String::as_str) + .collect::<BTreeSet<_>>(); + let expected_keys = [ + "id", + "pubkey", + "created_at", + "kind", + "tags", + "content", + "sig", + ] + .into_iter() + .collect::<BTreeSet<_>>(); + let (expected_tags, expected_content) = match witness { + RawEventVectorWitness::Profile => ( + json!([]), + "{\"name\":\"victoria-farm\",\"display_name\":\"Victoria Farm\",\"about\":\"Seasonal produce from the Saanich Peninsula\",\"picture\":\"https://media.example/512f6a371c77694502e7d08f0b1f1080c7103ca90925bfe2fa23106aac11003a.png\",\"banner\":\"https://media.example/fbf3819415a76ea0d3ba71817578bb89c4903aa958e38f76f86578dfa8a35145.webp\",\"nip05\":\"farm@example.com\",\"bot\":false}", + ), + RawEventVectorWitness::GenericRootNote => (json!([]), "Generic root note"), + RawEventVectorWitness::Reply => ( + json!([[ + "e", + "1111111111111111111111111111111111111111111111111111111111111111", + "", + "reply" + ]]), + "Reply", + ), + RawEventVectorWitness::Comment => ( + json!([[ + "E", + "1111111111111111111111111111111111111111111111111111111111111111" + ]]), + "Comment", + ), + RawEventVectorWitness::DeletionRequest => ( + json!([[ + "e", + "1111111111111111111111111111111111111111111111111111111111111111" + ]]), + "Removed", + ), + RawEventVectorWitness::CalendarDate => ( + json!([ + ["d", "farmers-market-2026"], + ["title", "Moss Street Farmers Market"], + ["start", "2026-07-25"], + ["end", "2026-07-26"], + ["location", "Victoria, BC"], + [ + "image", + "https://events.example/0a422cbf828d421341c40c678f4cfbd6451841760db126e5f5ac3d2e06fd80b8.jpeg" + ] + ]), + "Saturday market in Victoria", + ), + RawEventVectorWitness::CalendarTime => ( + json!([ + ["d", "farm-tour-2026"], + ["title", "Saanich Farm Tour"], + ["start", "1785003600"], + ["end", "1785007200"], + ["D", "20659"], + ["start_tzid", "America/Vancouver"], + [ + "image", + "https://events.example/0a422cbf828d421341c40c678f4cfbd6451841760db126e5f5ac3d2e06fd80b8.jpeg" + ] + ]), + "A one-hour farm tour", + ), + RawEventVectorWitness::CalendarCollection => (json!([["d", "victoria-calendar"]]), ""), + RawEventVectorWitness::CalendarRsvp => ( + json!([[ + "a", + "31922:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:market" + ]]), + "accepted", + ), + RawEventVectorWitness::Bud11Authorization => (json!([["t", "upload"]]), ""), + RawEventVectorWitness::Ephemeral => (json!([]), "Ephemeral"), + RawEventVectorWitness::Trade => (json!([["d", "trade-proposal"]]), ""), + RawEventVectorWitness::CommerceOrder => (json!([["d", "order-request"]]), ""), + RawEventVectorWitness::Group => (json!([["h", "victoria-growers"]]), ""), + RawEventVectorWitness::Operations => (json!([["d", "internal-operation"]]), ""), + }; + if actual_keys != expected_keys + || candidate.get("id").and_then(Value::as_str) != Some(VECTOR_ZERO_ID) + || candidate.get("pubkey").and_then(Value::as_str) != Some(VECTOR_AUTHOR) + || candidate.get("created_at").and_then(Value::as_u64) != Some(VECTOR_CREATED_AT) + || candidate.get("kind").and_then(Value::as_u64) != Some(u64::from(witness.event_kind())) + || candidate.get("tags") != Some(&expected_tags) + || candidate.get("content").and_then(Value::as_str) != Some(expected_content) + || candidate.get("sig").and_then(Value::as_str) != Some(VECTOR_ZERO_SIGNATURE) + { + return Err(format!( + "{case_id} raw event kind or exact required/forbidden tag shape drifted" + )); + } + Ok(()) +} + +fn validate_exact_tag_names( + case_id: &str, + tags: &[Value], + expected_names: &[&str], +) -> Result<(), String> { + let names = tags + .iter() + .map(|tag| tag.get(0).and_then(Value::as_str)) + .collect::<Option<Vec<_>>>() + .ok_or_else(|| format!("{case_id} contains an empty or non-string tag"))?; + if names != expected_names { + return Err(format!("{case_id} exact tag-name sequence drifted")); + } + Ok(()) +} + +fn require_exact_tag(case_id: &str, tags: &[Value], expected: &[&str]) -> Result<(), String> { + if tags.iter().any(|tag| { + tag.as_array().is_some_and(|fields| { + fields.len() == expected.len() + && fields + .iter() + .zip(expected) + .all(|(actual, expected)| actual.as_str() == Some(*expected)) + }) + }) { + Ok(()) + } else { + Err(format!("{case_id} is missing its exact semantic marker")) + } +} + +fn forbid_tag_name(case_id: &str, tags: &[Value], forbidden: &str) -> Result<(), String> { + if tags + .iter() + .any(|tag| tag.get(0).and_then(Value::as_str) == Some(forbidden)) + { + Err(format!("{case_id} contains forbidden marker {forbidden}")) + } else { + Ok(()) + } +} + +fn successor_source_specs(workspace_root: &Path) -> Result<Vec<(String, String)>, String> { + let mut specs = source_specs(workspace_root)?; + specs.extend([ + ( + "publication_allowlist_vector_executor".to_owned(), + VECTOR_EXECUTOR_RELATIVE.to_owned(), + ), + ( + "publication_allowlist_contract_governance".to_owned(), + "tools/xtask/src/contract/phase1_publication_allowlist.rs".to_owned(), + ), + ]); + specs.sort_by(|left, right| left.1.cmp(&right.1)); + let mut seen = BTreeSet::new(); + for (_, path) in &specs { + if !seen.insert(path.as_str()) { + return Err(format!( + "publication allowlist source inventory duplicates {path}" + )); + } + } + Ok(specs) +} + +fn validate_manifest_shape(manifest: &AllowlistManifest) -> Result<(), String> { + if manifest.schema_version != SCHEMA_VERSION + || manifest.contract_id != CONTRACT_ID + || manifest.authority_id != AUTHORITY_ID + || manifest.predecessor.contract_id != PREDECESSOR_CONTRACT_ID + || manifest.allowlist != expected_allowlist_descriptor() + || manifest.predecessor_source_supersessions + != PUBLICATION_SUCCESSOR_SUPERSEDED_PATHS + .iter() + .map(|path| (*path).to_owned()) + .collect::<Vec<_>>() + || manifest.result_vector.canonical_path != VECTOR_RELATIVE + || manifest.result_vector.mirror_path != VECTOR_MIRROR_RELATIVE + || manifest.release.change_id != RELEASE_CHANGE_ID + { + return Err(format!("{MANIFEST_RELATIVE} shape drifted")); + } + let expected_predecessor = PREDECESSOR_ARTIFACTS + .iter() + .map(|spec| (spec.relative, spec.byte_length as u64, spec.sha256)) + .collect::<Vec<_>>(); + let actual_predecessor = manifest + .predecessor + .immutable_artifacts + .iter() + .map(|descriptor| { + ( + descriptor.path.as_str(), + descriptor.byte_length, + descriptor.sha256.as_str(), + ) + }) + .collect::<Vec<_>>(); + if actual_predecessor != expected_predecessor + || manifest.event_contract_registry.version != 7 + || manifest.event_contract_registry.inventory.byte_length != REGISTRY_BYTE_LENGTH as u64 + || manifest.event_contract_registry.inventory.sha256 != REGISTRY_SHA256 + || manifest.event_contract_registry.sidecar.byte_length + != REGISTRY_SIDECAR_BYTE_LENGTH as u64 + || manifest.event_contract_registry.sidecar.sha256 != REGISTRY_SIDECAR_SHA256 + { + return Err(format!( + "{MANIFEST_RELATIVE} predecessor or registry identity drifted" + )); + } + let mut paths = BTreeSet::new(); + for source in &manifest.source_files { + if source.hash_algorithm != HASH_ALGORITHM || !paths.insert(source.path.as_str()) { + return Err(format!("{MANIFEST_RELATIVE} source inventory is invalid")); + } + } + Ok(()) +} + +fn manifest_schema() -> Value { + json!({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://radroots.org/contracts/event-codec/phase1-publication-allowlist-v1.schema.json", + "title": "Radroots Phase 1 Publication Allowlist Contract", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", "contract_id", "authority_id", "manifest_schema", "predecessor", + "event_contract_registry", "allowlist", "predecessor_source_supersessions", + "source_files", "result_vector", "release" + ], + "properties": { + "schema_version": {"const": SCHEMA_VERSION}, + "contract_id": {"const": CONTRACT_ID}, + "authority_id": {"const": AUTHORITY_ID}, + "manifest_schema": {"$ref": "#/$defs/file"}, + "predecessor": { + "type": "object", "additionalProperties": false, + "required": ["contract_id", "immutable_artifacts"], + "properties": { + "contract_id": {"const": PREDECESSOR_CONTRACT_ID}, + "immutable_artifacts": {"type": "array", "minItems": 6, "maxItems": 6, "items": {"$ref": "#/$defs/file"}} + } + }, + "event_contract_registry": { + "type": "object", "additionalProperties": false, + "required": ["version", "inventory", "sidecar", "evolution"], + "properties": { + "version": {"const": 7}, + "inventory": {"$ref": "#/$defs/file"}, + "sidecar": {"$ref": "#/$defs/file"}, + "evolution": {"const": "immutable_registry_v7_plus_additive_phase1_publication_allowlist_v1"} + } + }, + "allowlist": { + "type": "object", "additionalProperties": false, + "required": ["version", "contract_id", "operation_id", "canonical_json_operation_id", "input_type", "output_type", "allowed_leaves", "kind_one_precedence", "event_policy", "classified_listing_policy", "denied_families", "granted_capability", "excluded_capabilities"], + "properties": { + "version": {"const": 1}, + "contract_id": {"const": RADROOTS_PHASE1_PUBLICATION_ALLOWLIST_CONTRACT_ID}, + "operation_id": {"const": OPERATION_ID}, + "canonical_json_operation_id": {"const": CANONICAL_JSON_OPERATION_ID}, + "input_type": {"const": "RadrootsPhase1PublicationArtifact"}, + "output_type": {"const": "RadrootsPhase1AllowlistedPublicationArtifact"}, + "allowed_leaves": {"type": "array", "minItems": 7, "maxItems": 7, "items": {"$ref": "#/$defs/leaf"}}, + "kind_one_precedence": {"const": ["ask", "photo_update", "update"]}, + "event_policy": {"const": "strict_typed_nip52_date_or_time_artifact_only_v1"}, + "classified_listing_policy": {"const": "raw_marker_partition_before_focused_food_profile_validation_v1"}, + "denied_families": {"type": "array", "minItems": 19, "maxItems": 19, "items": {"type": "string", "minLength": 1}}, + "granted_capability": {"const": "phase1_durable_publication_lane_entry_only_v1"}, + "excluded_capabilities": {"type": "array", "minItems": 7, "maxItems": 7, "items": {"type": "string", "minLength": 1}} + } + }, + "predecessor_source_supersessions": {"type": "array", "minItems": 8, "maxItems": 8, "items": {"type": "string", "minLength": 1}}, + "source_files": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/source"}}, + "result_vector": { + "type": "object", "additionalProperties": false, + "required": ["canonical_path", "mirror_path", "byte_length", "sha256", "hash_algorithm", "executor_path", "executor_test", "valid_case_ids", "invalid_case_ids"], + "properties": { + "canonical_path": {"const": VECTOR_RELATIVE}, + "mirror_path": {"const": VECTOR_MIRROR_RELATIVE}, + "byte_length": {"type": "integer", "minimum": 1}, + "sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, + "hash_algorithm": {"const": HASH_ALGORITHM}, + "executor_path": {"const": VECTOR_EXECUTOR_RELATIVE}, + "executor_test": {"const": VECTOR_EXECUTOR_TEST}, + "valid_case_ids": {"type": "array", "minItems": 14, "maxItems": 14, "items": {"type": "string"}}, + "invalid_case_ids": {"type": "array", "minItems": 25, "maxItems": 25, "items": {"type": "string"}} + } + }, + "release": { + "type": "object", "additionalProperties": false, + "required": ["record_path", "change_id", "changelog_path", "changelog_marker"], + "properties": { + "record_path": {"const": RELEASE_RELATIVE}, + "change_id": {"const": RELEASE_CHANGE_ID}, + "changelog_path": {"const": CHANGELOG_RELATIVE}, + "changelog_marker": {"const": CHANGELOG_MARKER} + } + } + }, + "$defs": { + "file": { + "type": "object", "additionalProperties": false, + "required": ["path", "byte_length", "sha256", "hash_algorithm"], + "properties": { + "path": {"type": "string", "minLength": 1}, + "byte_length": {"type": "integer", "minimum": 1}, + "sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, + "hash_algorithm": {"const": HASH_ALGORITHM} + } + }, + "source": { + "type": "object", "additionalProperties": false, + "required": ["role", "path", "byte_length", "sha256", "hash_algorithm"], + "properties": { + "role": {"type": "string", "minLength": 1}, + "path": {"type": "string", "minLength": 1}, + "byte_length": {"type": "integer", "minimum": 1}, + "sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, + "hash_algorithm": {"const": HASH_ALGORITHM} + } + }, + "leaf": { + "type": "object", "additionalProperties": false, + "required": ["leaf", "kind", "publication_role", "authored_operation_id", "event_contract_id"], + "properties": { + "leaf": {"type": "string", "minLength": 1}, + "kind": {"type": "integer", "minimum": 0}, + "publication_role": {"enum": ["supporting", "root"]}, + "authored_operation_id": {"type": "string", "minLength": 1}, + "event_contract_id": {"type": "string", "minLength": 1} + } + } + } + }) +} + +fn source_descriptor( + workspace_root: &Path, + role: &str, + path: &str, +) -> Result<SourceFileDescriptor, String> { + let bytes = read_regular_file(workspace_root, path)?; + Ok(SourceFileDescriptor { + role: role.to_owned(), + path: path.to_owned(), + byte_length: bytes.len() as u64, + sha256: sha256_hex(&bytes), + hash_algorithm: HASH_ALGORITHM.to_owned(), + }) +} + +fn descriptor_for_file(workspace_root: &Path, path: &str) -> Result<FileDescriptor, String> { + descriptor_for_bytes(path, &read_regular_file(workspace_root, path)?) +} + +fn descriptor_for_bytes(path: &str, bytes: &[u8]) -> Result<FileDescriptor, String> { + Ok(FileDescriptor { + path: path.to_owned(), + byte_length: bytes.len() as u64, + sha256: sha256_hex(bytes), + hash_algorithm: HASH_ALGORITHM.to_owned(), + }) +} + +fn parse_toml(workspace_root: &Path, relative: &str) -> Result<toml::Value, String> { + let bytes = read_regular_file(workspace_root, relative)?; + let source = std::str::from_utf8(&bytes) + .map_err(|error| format!("{relative} must be UTF-8: {error}"))?; + toml::from_str(source).map_err(|error| format!("parse {relative}: {error}")) +} + +fn toml_string_array(label: &str, value: Option<&toml::Value>) -> Result<Vec<String>, String> { + value + .and_then(toml::Value::as_array) + .ok_or_else(|| format!("{label} must be an array"))? + .iter() + .map(|value| { + value + .as_str() + .map(str::to_owned) + .ok_or_else(|| format!("{label} values must be strings")) + }) + .collect() +} + +fn canonical_json_bytes<T: Serialize>(value: &T) -> Result<Vec<u8>, String> { + let mut bytes = + serde_json::to_vec_pretty(value).map_err(|error| format!("serialize JSON: {error}"))?; + bytes.push(b'\n'); + Ok(bytes) +} + +fn validate_canonical_json<T: Serialize>( + relative: &str, + bytes: &[u8], + value: &T, +) -> Result<(), String> { + if canonical_json_bytes(value)? != bytes { + return Err(format!("{relative} is not canonical pretty JSON")); + } + Ok(()) +} + +fn validate_json_schema(schema: &Value, instance: &Value) -> Result<(), String> { + let validator = jsonschema::validator_for(schema) + .map_err(|error| format!("compile {MANIFEST_SCHEMA_RELATIVE}: {error}"))?; + let errors = validator + .iter_errors(instance) + .map(|error| error.to_string()) + .collect::<Vec<_>>(); + if errors.is_empty() { + Ok(()) + } else { + Err(format!( + "{MANIFEST_RELATIVE} violates its schema: {}", + errors.join("; ") + )) + } +} + +fn sha256_hex(bytes: &[u8]) -> String { + hex::encode(Sha256::digest(bytes)) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + + fn workspace_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .expect("xtask workspace root") + .to_path_buf() + } + + #[test] + fn allowlist_source_inventory_is_closed_and_unique() { + let specs = successor_source_specs(&workspace_root()).expect("allowlist sources"); + let paths = specs + .iter() + .map(|(_, path)| path.as_str()) + .collect::<BTreeSet<_>>(); + assert_eq!(paths.len(), specs.len()); + assert!(paths.contains("crates/event_codec/src/wire/publication/allowlist.rs")); + assert!(paths.contains(VECTOR_EXECUTOR_RELATIVE)); + assert!(paths.contains("tools/xtask/src/contract/phase1_publication_allowlist.rs")); + for generated in GENERATED_ARTIFACT_PATHS { + assert!(!paths.contains(generated)); + } + } + + #[test] + fn allowlist_authorities_and_vector_are_current() { + let root = workspace_root(); + validate_registry_identity(&root).expect("registry identity"); + validate_operations_authority(&root).expect("operation authority"); + validate_release_authority(&root).expect("release authority"); + validate_vector(&root).expect("allowlist vector"); + } + + #[test] + fn allowlist_vector_semantic_witnesses_fail_closed() { + let bytes = read_regular_file(&workspace_root(), VECTOR_RELATIVE).expect("vector bytes"); + let suite: VectorSuite = serde_json::from_slice(&bytes).expect("vector suite"); + for (spec, vector) in INVALID_VECTOR_SPECS + .iter() + .copied() + .zip(suite.vectors.iter().skip(VALID_VECTOR_SPECS.len())) + { + let input = vector.input.as_object().expect("vector input"); + let canonical_json = input + .get("canonical_json") + .and_then(Value::as_str) + .expect("canonical candidate"); + let mut candidate: Value = + serde_json::from_str(canonical_json).expect("candidate JSON"); + match spec.witness { + InvalidVectorWitness::Artifact(_) => { + candidate["draft"]["content"] = Value::String("witness drift".to_owned()); + } + InvalidVectorWitness::RawEvent(_) => { + candidate["content"] = Value::String("witness drift".to_owned()); + } + InvalidVectorWitness::NonEventRouteDelivery => { + candidate["product_surface"] = Value::String("delivery_event".to_owned()); + } + } + assert!( + validate_invalid_vector_evidence(spec.id, spec, input, &candidate).is_err(), + "{} semantic witness accepted drift", + spec.id + ); + } + } + + #[test] + fn allowlist_manifest_schema_rejects_unknown_top_level_fields() { + let root = workspace_root(); + let schema = manifest_schema(); + let schema_bytes = canonical_json_bytes(&schema).expect("schema bytes"); + let manifest = describe_manifest(&root, &schema_bytes).expect("manifest"); + let mut value = serde_json::to_value(manifest).expect("manifest value"); + validate_json_schema(&schema, &value).expect("valid manifest"); + value["unexpected"] = Value::Bool(true); + validate_json_schema(&schema, &value).expect_err("unknown field must fail"); + } +} diff --git a/tools/xtask/src/contract/phase1_publication_artifact.rs b/tools/xtask/src/contract/phase1_publication_artifact.rs @@ -245,6 +245,50 @@ const RAW_IMMUTABLE_ARTIFACTS: &[ImmutableArtifactSpec] = &[ ), ]; +const PUBLICATION_IMMUTABLE_ARTIFACTS: &[ImmutableArtifactSpec] = &[ + ImmutableArtifactSpec::new( + MANIFEST_RELATIVE, + 89_464, + "0776e1d84c9366954047e75cdf12d9acc9a7108260157c3534f22067075a385a", + ), + ImmutableArtifactSpec::new( + MANIFEST_SCHEMA_RELATIVE, + 11_972, + "1d72cee2754e7ac45105d79b1ecf7d44251991be7a18ba106166e962000e8320", + ), + ImmutableArtifactSpec::new( + MANIFEST_SHA256_RELATIVE, + 65, + "586c0985b502f22241b4d90f2ecb475d43953fc3ddd66d6fbfa3b3eb9cf34444", + ), + ImmutableArtifactSpec::new( + VECTOR_RELATIVE, + 23_113, + "ec18c687d5b0710a48624ddb620d89157e6b645dbea8bb91c62e3a111d20c622", + ), + ImmutableArtifactSpec::new( + VECTOR_MIRROR_RELATIVE, + 23_113, + "ec18c687d5b0710a48624ddb620d89157e6b645dbea8bb91c62e3a111d20c622", + ), + ImmutableArtifactSpec::new( + VECTOR_EXECUTOR_RELATIVE, + 34_582, + "7a31169eac4217a38cb3ef25eb9213f2f89e11fb17e76ceaf7449b34225e98af", + ), +]; + +pub(super) const PUBLICATION_SUCCESSOR_SUPERSEDED_PATHS: &[&str] = &[ + CHANGELOG_RELATIVE, + OPERATIONS_RELATIVE, + RELEASE_RELATIVE, + "crates/event_codec/README", + "crates/event_codec/src/wire/publication.rs", + "tools/xtask/src/contract.rs", + "tools/xtask/src/contract/phase1_publication_artifact.rs", + "tools/xtask/src/main.rs", +]; + const GOVERNED_COMPILER_TABLES: &[(&str, &str, &str)] = &[ ( "crates/core/Cargo.toml", @@ -542,17 +586,24 @@ struct ValidatedResultVector { pub(crate) fn write_phase1_publication_artifact_manifest( workspace_root: &Path, ) -> Result<(), String> { - with_artifact_bundle_transaction(workspace_root, |transaction| { - transaction.write(expected_artifacts(workspace_root)?)?; - validate_manifest_under_lock(workspace_root) - }) + validate_immutable_phase1_publication_artifact_predecessor(workspace_root)?; + Err( + "Phase 1 publication artifact v1 is an immutable predecessor and cannot be rewritten; write the active Phase 1 publication allowlist successor instead" + .to_owned(), + ) } pub(crate) fn validate_phase1_publication_artifact_manifest( workspace_root: &Path, ) -> Result<(), String> { + validate_immutable_phase1_publication_artifact_predecessor(workspace_root) +} + +pub(crate) fn validate_immutable_phase1_publication_artifact_predecessor( + workspace_root: &Path, +) -> Result<(), String> { with_artifact_bundle_transaction(workspace_root, |_| { - validate_manifest_under_lock(workspace_root) + validate_immutable_phase1_publication_artifact_predecessor_under_lock(workspace_root) }) } @@ -599,6 +650,93 @@ fn validate_manifest_under_lock(workspace_root: &Path) -> Result<(), String> { Ok(()) } +pub(super) fn validate_immutable_phase1_publication_artifact_predecessor_under_lock( + workspace_root: &Path, +) -> Result<(), String> { + for spec in PUBLICATION_IMMUTABLE_ARTIFACTS { + let bytes = read_regular_file(workspace_root, spec.relative)?; + if bytes.len() != spec.byte_length || sha256_hex(&bytes) != spec.sha256 { + return Err(format!( + "immutable Phase 1 publication artifact predecessor {} drifted", + spec.relative + )); + } + } + + let manifest_bytes = read_regular_file(workspace_root, MANIFEST_RELATIVE)?; + let manifest: Value = serde_json::from_slice(&manifest_bytes) + .map_err(|error| format!("parse {MANIFEST_RELATIVE}: {error}"))?; + if manifest.get("schema_version").and_then(Value::as_u64) != Some(1) + || manifest.get("contract_id").and_then(Value::as_str) + != Some("radroots_event_codec.phase1_publication_artifact_v1") + || manifest.get("authority_id").and_then(Value::as_str) + != Some("phase1_publication_artifact_v1") + { + return Err(format!("{MANIFEST_RELATIVE} identity drifted")); + } + let sidecar = read_regular_file(workspace_root, MANIFEST_SHA256_RELATIVE)?; + if sidecar != format!("{}\n", sha256_hex(&manifest_bytes)).as_bytes() { + return Err(format!( + "{MANIFEST_SHA256_RELATIVE} does not authenticate its immutable manifest" + )); + } + + let superseded = PUBLICATION_SUCCESSOR_SUPERSEDED_PATHS + .iter() + .copied() + .collect::<BTreeSet<_>>(); + if superseded.len() != PUBLICATION_SUCCESSOR_SUPERSEDED_PATHS.len() { + return Err("publication predecessor supersession paths must be unique".to_owned()); + } + let sources = manifest + .get("source_files") + .and_then(Value::as_array) + .ok_or_else(|| format!("{MANIFEST_RELATIVE} has no source_files array"))?; + let mut seen = BTreeSet::new(); + for source in sources { + let path = descriptor_path(source, "publication predecessor source")?; + if !seen.insert(path) { + return Err(format!("{MANIFEST_RELATIVE} duplicates source {path}")); + } + if !superseded.contains(path) { + validate_value_descriptor(workspace_root, source, "publication predecessor source")?; + } + } + for path in superseded { + if !seen.contains(path) { + return Err(format!( + "publication predecessor supersession path {path} is not predecessor-bound" + )); + } + } + + validate_value_descriptor( + workspace_root, + manifest + .get("manifest_schema") + .ok_or_else(|| format!("{MANIFEST_RELATIVE} has no manifest_schema"))?, + "publication predecessor schema", + )?; + for pointer in [ + "/event_contract_registry/inventory", + "/event_contract_registry/sidecar", + ] { + validate_value_descriptor( + workspace_root, + manifest + .pointer(pointer) + .ok_or_else(|| format!("{MANIFEST_RELATIVE} has no {pointer}"))?, + "publication predecessor registry", + )?; + } + if read_regular_file(workspace_root, VECTOR_RELATIVE)? + != read_regular_file(workspace_root, VECTOR_MIRROR_RELATIVE)? + { + return Err("immutable Phase 1 publication artifact vector mirror drifted".to_owned()); + } + Ok(()) +} + fn expected_artifacts(workspace_root: &Path) -> Result<Vec<GeneratedArtifact>, String> { validate_immutable_raw_predecessor_under_lock(workspace_root)?; validate_source_contract(workspace_root)?; @@ -1345,7 +1483,7 @@ fn descriptor_path<'a>(descriptor: &'a Value, label: &str) -> Result<&'a str, St .ok_or_else(|| format!("{label} has no path")) } -fn source_specs(workspace_root: &Path) -> Result<Vec<(String, String)>, String> { +pub(super) fn source_specs(workspace_root: &Path) -> Result<Vec<(String, String)>, String> { let mut specs = Vec::new(); for root in PUBLIC_SOURCE_ROOTS { for path in regular_file_inventory(workspace_root, root)? { diff --git a/tools/xtask/src/main.rs b/tools/xtask/src/main.rs @@ -23,6 +23,7 @@ fn usage() { eprintln!(" cargo xtask contract source-maintenance-manifest [--write]"); eprintln!(" cargo xtask contract raw-source-rebuild-manifest [--write]"); eprintln!(" cargo xtask contract phase1-publication-artifact-manifest [--write]"); + eprintln!(" cargo xtask contract phase1-publication-allowlist-manifest [--write]"); eprintln!(" cargo xtask contract knowledge-manifest [--write]"); eprintln!(" cargo xtask dto-roots --check|--write"); eprintln!(" cargo xtask release preflight"); @@ -149,6 +150,16 @@ fn run_contract(args: &[String]) -> Result<(), String> { .to_string(), ), }, + Some("phase1-publication-allowlist-manifest") => match &args[1..] { + [] => contract::validate_phase1_publication_allowlist_manifest(&workspace_root()), + [flag] if flag == "--write" => { + contract::write_phase1_publication_allowlist_manifest(&workspace_root()) + } + _ => Err( + "phase1-publication-allowlist-manifest accepts no arguments or exactly --write" + .to_string(), + ), + }, Some("knowledge-manifest") => { if args.get(1).map(String::as_str) == Some("--write") { contract::write_knowledge_contract_manifest(&workspace_root()) @@ -281,6 +292,12 @@ mod tests { ]) .expect_err("invalid Phase 1 publication manifest mode"); assert!(invalid_publication.contains("exactly --write")); + let invalid_publication_allowlist = run_contract(&[ + "phase1-publication-allowlist-manifest".to_string(), + "--invalid".to_string(), + ]) + .expect_err("invalid Phase 1 publication allowlist manifest mode"); + assert!(invalid_publication_allowlist.contains("exactly --write")); let unknown_root = run(&["unknown".to_string()]).expect_err("unknown command"); assert!(unknown_root.contains("unknown command")); @@ -384,6 +401,8 @@ mod tests { .expect("contract raw-source rebuild manifest"); run_contract(&["phase1-publication-artifact-manifest".to_string()]) .expect("contract Phase 1 publication artifact manifest"); + run_contract(&["phase1-publication-allowlist-manifest".to_string()]) + .expect("contract Phase 1 publication allowlist manifest"); run_contract(&["knowledge-manifest".to_string()]).expect("contract knowledge manifest"); } }