lib

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

commit 88f8731ac249a09f0d9c4ae911fe0da9fc989a52
parent 4d335832d005e29c675b6109be91eb7eec6cfa0f
Author: triesap <tyson@radroots.org>
Date:   Wed, 22 Jul 2026 04:23:25 +0000

event-codec: integrate Phase 1 publication artifact

- seal Profile and five Phase 1 root publications behind strict authored constructors
- persist exact drafts, authors, Blossom media inventory, and domain-separated digests
- freeze the corrected raw-source predecessor without changing registry v7
- bind canonical vectors and generated authority to the corrected integration base

Diffstat:
MCHANGELOG.md | 13+++++++++++++
Acontracts/conformance/vectors/publication/phase1_artifact.v1.json | 335+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcontracts/operations.toml | 252+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcontracts/releases/1.0.0-alpha.1.toml | 11+++++++++++
Mcrates/event_codec/Cargo.toml | 2+-
Mcrates/event_codec/README | 23+++++++++++++++++++++++
Acrates/event_codec/contracts/phase1_publication_artifact_v1.manifest.json | 2387+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/event_codec/contracts/phase1_publication_artifact_v1.manifest.schema.json | 493+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/event_codec/contracts/phase1_publication_artifact_v1.manifest.sha256 | 1+
Mcrates/event_codec/src/lib.rs | 2+-
Mcrates/event_codec/src/wire.rs | 3+++
Acrates/event_codec/src/wire/publication.rs | 1596+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/event_codec/tests/codec_error_job.rs | 13+++++++++++++
Acrates/event_codec/tests/fixtures/phase1_publication_artifact.v1.json | 335+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/event_codec/tests/publication_artifact.rs | 936+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtools/xtask/src/contract.rs | 24+++++++++++++++++++++---
Mtools/xtask/src/contract/nip09_reconciliation.rs | 38+++++++++++++++++++++++++++++++++++++-
Atools/xtask/src/contract/phase1_publication_artifact.rs | 1819+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtools/xtask/src/contract/raw_source_rebuild.rs | 31+++++++++++++++----------------
Mtools/xtask/src/main.rs | 19+++++++++++++++++++
20 files changed, 8311 insertions(+), 22 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -171,6 +171,19 @@ publish policy both pass for the same source revision. The executable raw-rebuild successor contract freezes the SourceMaintenance predecessor and the `0001` through `0004` migration inventory; no schema migration is added. +<!-- release-change: phase1-publication-artifact --> +- The `serde_json` event-codec surface now exposes a sealed Phase 1 publication + artifact constructed only from strict authored Profile, Update, PhotoUpdate, + Ask, date/time Event, and FoodAvailability models. The exact version-1 + envelope places the expected NIP-01 id at top level after the frozen draft, + exposes only explicit canonical-byte encode/decode operations, and binds all + fields except the digest under an ASCII-domain, single-NUL SHA-256 preimage. + Construction and reload enforce the 2 MiB artifact, 256 KiB signed-event, + and 4,096-reference bounds plus complete case-preserving Blossom URL + commitments. Strict reload rejects unknown or duplicate fields, alternate + encodings, cross-profile promotion, stale identifiers, media drift, and + digest tampering without claiming restored byte verification, upload + completion, signature authenticity, or signer 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_artifact.v1.json b/contracts/conformance/vectors/publication/phase1_artifact.v1.json @@ -0,0 +1,335 @@ +{ + "suite": "phase1_publication_artifact", + "contract_version": "1.0.0", + "vectors": [ + { + "id": "profile_round_trip", + "kind": "publication_artifact.build_profile.valid", + "input": { "fixture": "profile" }, + "expected": { + "semantic_variant": "profile", + "operation_id": "profile.build_authored_draft", + "contract_id": "radroots.profile.metadata.v1", + "event_kind": 0, + "media_count": 2, + "expected_event_id": "a7d081b9c142e3a68245bafe1921b0e0dd88f886406e7165a9729f6c57026289", + "artifact_digest": "e296ed63312d58b4e3ee160d4ca3457eb922920d3116329ca498f08e402215b0", + "canonical_json_bytes": 1305, + "canonical_json_sha256": "131d8131bb821179e6fd64c5201d01bee384451d09c20d789860439ef05620e8", + "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\"}" + } + }, + { + "id": "update_round_trip", + "kind": "publication_artifact.build_update.valid", + "input": { "fixture": "update" }, + "expected": { + "semantic_variant": "update", + "operation_id": "social.update.build_authored_draft", + "contract_id": "radroots.social.update.v1", + "event_kind": 1, + "media_count": 0, + "expected_event_id": "7f2d9dbbd6d2f3db1e83338ea2e669b2458e62ded159de15a3fa98dcf9f164e3", + "artifact_digest": "9ad318496bd4a710fbc7f3e0f6d5a01de808d352db91ca56a12e710904784cc5", + "canonical_json_bytes": 525, + "canonical_json_sha256": "21be0d18a7f5812f7fa14600f46e0948480d2525eb1dcab416bb4ef99ca731c6", + "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\"}" + } + }, + { + "id": "photo_update_round_trip", + "kind": "publication_artifact.build_photo_update.valid", + "input": { "fixture": "photo_update" }, + "expected": { + "semantic_variant": "photo_update", + "operation_id": "social.photo_update.build_authored_draft", + "contract_id": "radroots.social.photo_update.v1", + "event_kind": 1, + "media_count": 2, + "expected_event_id": "e592229776cd2a0d0e25a701f4629c0a5dc6e08481a235623d50da6ee8e8f438", + "artifact_digest": "9ed6b7a6fcec3b07bd0c9179fd5b6ecf74517390051e3c2bd1659be62a26c197", + "canonical_json_bytes": 1415, + "canonical_json_sha256": "38caab5242ae70fe2e36b39658dba7e6f723b2bd4fa512c8f166387c17faf69c", + "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\"}" + } + }, + { + "id": "ask_round_trip_with_fallback", + "kind": "publication_artifact.build_ask.valid", + "input": { "fixture": "ask" }, + "expected": { + "semantic_variant": "ask", + "operation_id": "social.ask.build_authored_draft", + "contract_id": "radroots.social.ask.v1", + "event_kind": 1, + "media_count": 2, + "expected_event_id": "bd45fbbb5bfb8a5aa32df81d240e5f3bd56b0f8cde3a20b3f7d455801373fe48", + "artifact_digest": "41eddea199bba97adebef4ce433ebfa05d97978ce8464ed3d52762ab717c2a3c", + "canonical_json_bytes": 1411, + "canonical_json_sha256": "4985ad0919758e3747f0548409854deb77b9734825ab597623e5c6b566e2d5c7", + "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\"}" + } + }, + { + "id": "event_date_round_trip", + "kind": "publication_artifact.build_calendar_date_event.valid", + "input": { "fixture": "event_date" }, + "expected": { + "semantic_variant": "event_date", + "operation_id": "social.calendar_date_event.build_authored_draft", + "contract_id": "radroots.calendar.date_event.v1", + "event_kind": 31922, + "media_count": 1, + "expected_event_id": "bea82662b6acf2f1272c8335a24bb0957e35d22c2b29d3e30c094ad7398dc7c7", + "artifact_digest": "0ec5162ab80988999aac62329d4cfc9c7efce6a82bbda539a723347d73596eb3", + "canonical_json_bytes": 1013, + "canonical_json_sha256": "46b015e34556762f3d7ca592cd3dc6d4d5652ff085c7d8a8dc458d5897cf0b22", + "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\"}" + } + }, + { + "id": "event_time_round_trip", + "kind": "publication_artifact.build_calendar_time_event.valid", + "input": { "fixture": "event_time" }, + "expected": { + "semantic_variant": "event_time", + "operation_id": "social.calendar_time_event.build_authored_draft", + "contract_id": "radroots.calendar.time_event.v1", + "event_kind": 31923, + "media_count": 1, + "expected_event_id": "2088ed11cd8c1495a82e6f5198ed2ee98e0cbc599f22d7f3380892155dc55894", + "artifact_digest": "56bfb4bed87559a58922343905a73c0c96dd0a57f94ff0955caecf0589198638", + "canonical_json_bytes": 1013, + "canonical_json_sha256": "58e3c7e684bc774c35b08b246075076b5286d51639eb0651ed5b6362d958639e", + "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\"}" + } + }, + { + "id": "food_availability_round_trip", + "kind": "publication_artifact.build_food_availability.valid", + "input": { "fixture": "food_availability" }, + "expected": { + "semantic_variant": "food_availability", + "operation_id": "food_availability.build_authored_draft", + "contract_id": "radroots.food.availability.v1", + "event_kind": 30402, + "media_count": 1, + "expected_event_id": "76b60f1b178c0dbdc14a87de36151ea6b04a866ce50e34a0ccae2f406f514e81", + "artifact_digest": "f0989cb993194af4a2f907ad8337e50378d2757c536a09afb4055a681043a7d8", + "canonical_json_bytes": 1132, + "canonical_json_sha256": "525dc96e87b79b1ef3aa67c711cccd8b8e6c205665770ebcb8283035b7b496da", + "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\"}" + } + }, + { + "id": "canonical_json_serialization", + "kind": "publication_artifact.to_canonical_json.valid", + "input": { "fixture": "profile" }, + "expected": { + "canonical_json_bytes": 1305, + "canonical_json_sha256": "131d8131bb821179e6fd64c5201d01bee384451d09c20d789860439ef05620e8" + } + }, + { + "id": "canonical_json_reload", + "kind": "publication_artifact.from_canonical_json.valid", + "input": { "fixture": "profile" }, + "expected": { + "semantic_variant": "profile", + "canonical_json_sha256": "131d8131bb821179e6fd64c5201d01bee384451d09c20d789860439ef05620e8" + } + }, + { + "id": "leading_whitespace_is_noncanonical", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "leading_whitespace" }, + "expected": { "error": "publication_artifact_non_canonical_json" } + }, + { + "id": "artifact_exact_byte_limit_reaches_parser", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "artifact_exact_byte_limit" }, + "expected": { "error": "publication_artifact_invalid_json" } + }, + { + "id": "artifact_one_byte_over_limit_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "artifact_one_over_byte_limit" }, + "expected": { "error": "publication_artifact_too_large" } + }, + { + "id": "unknown_field_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "unknown_field" }, + "expected": { "error": "publication_artifact_invalid_json" } + }, + { + "id": "unknown_draft_field_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "unknown_draft_field" }, + "expected": { "error": "publication_artifact_invalid_json" } + }, + { + "id": "nested_expected_event_id_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "nested_expected_event_id" }, + "expected": { "error": "publication_artifact_invalid_json" } + }, + { + "id": "missing_expected_event_id_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "missing_expected_event_id" }, + "expected": { "error": "publication_artifact_invalid_json" } + }, + { + "id": "malformed_expected_event_id_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "malformed_expected_event_id" }, + "expected": { "error": "publication_expected_event_id_invalid" } + }, + { + "id": "uppercase_expected_event_id_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "uppercase_expected_event_id" }, + "expected": { "error": "publication_expected_event_id_invalid" } + }, + { + "id": "duplicate_expected_event_id_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "duplicate_expected_event_id" }, + "expected": { "error": "publication_artifact_invalid_json" } + }, + { + "id": "unknown_media_field_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "unknown_media_field" }, + "expected": { "error": "publication_artifact_invalid_json" } + }, + { + "id": "json_field_order_is_noncanonical", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "json_field_order" }, + "expected": { "error": "publication_artifact_non_canonical_json" } + }, + { + "id": "unknown_version_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "schema_version" }, + "expected": { "error": "publication_artifact_version_unsupported" } + }, + { + "id": "cross_variant_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "all_cross_variants" }, + "expected": { "error": "publication_authored_operation_mismatch" } + }, + { + "id": "operation_mismatch_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "operation_id" }, + "expected": { "error": "publication_authored_operation_mismatch" } + }, + { + "id": "contract_mismatch_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "contract_id" }, + "expected": { "error": "publication_event_contract_mismatch" } + }, + { + "id": "kind_mismatch_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "kind" }, + "expected": { "error": "publication_kind_mismatch" } + }, + { + "id": "author_mismatch_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "author" }, + "expected": { "error": "publication_expected_author_invalid" } + }, + { + "id": "created_at_tamper_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "created_at" }, + "expected": { "error": "publication_expected_event_id_mismatch" } + }, + { + "id": "draft_tags_tamper_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "draft_tags" }, + "expected": { "error": "publication_expected_event_id_mismatch" } + }, + { + "id": "draft_content_tamper_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "draft_content" }, + "expected": { "error": "publication_expected_event_id_mismatch" } + }, + { + "id": "noncanonical_nip05_is_rejected_after_id_rebuild", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "noncanonical_nip05" }, + "expected": { "error": "publication_profile_invalid" } + }, + { + "id": "empty_ask_content_is_rejected_after_id_rebuild", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "ask", "mutation": "empty_ask_content" }, + "expected": { "error": "publication_post_profile_invalid" } + }, + { + "id": "expected_event_id_tamper_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "expected_event_id" }, + "expected": { "error": "publication_expected_event_id_mismatch" } + }, + { + "id": "digest_tamper_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "digest" }, + "expected": { "error": "publication_artifact_digest_mismatch" } + }, + { + "id": "media_order_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "media_order" }, + "expected": { "error": "publication_media_inventory_non_canonical" } + }, + { + "id": "profile_media_commitment_tamper_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "media_size" }, + "expected": { "error": "publication_artifact_digest_mismatch" } + }, + { + "id": "media_url_tamper_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "media_url" }, + "expected": { "error": "publication_media_inventory_mismatch" } + }, + { + "id": "noncanonical_media_url_casing_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "media_url_casing" }, + "expected": { "error": "publication_media_reference_invalid" } + }, + { + "id": "media_hash_tamper_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "media_hash" }, + "expected": { "error": "publication_media_reference_invalid" } + }, + { + "id": "media_type_tamper_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "media_type" }, + "expected": { "error": "publication_artifact_digest_mismatch" } + }, + { + "id": "post_media_commitment_must_match_imeta", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "photo_update", "mutation": "media_size" }, + "expected": { "error": "publication_media_inventory_mismatch" } + } + ] +} diff --git a/contracts/operations.toml b/contracts/operations.toml @@ -15,6 +15,7 @@ domains = [ "trade_validation", "social", "knowledge", + "publication", ] [shared_types] @@ -98,6 +99,13 @@ public = [ "RadrootsProfileAdmissionError", "RadrootsNip05IdentityVerification", "RadrootsUnverifiedProfileMediaReference", + "RadrootsPhase1PublicationEventVariant", + "RadrootsPhase1PublicationSemanticVariant", + "RadrootsPhase1PublicationDraft", + "RadrootsPhase1PublicationMediaReference", + "RadrootsPhase1PublicationArtifactDigest", + "RadrootsPhase1PublicationArtifact", + "RadrootsPhase1PublicationArtifactError", "RadrootsFarm", "RadrootsOperationalListing", "RadrootsPost", @@ -254,6 +262,7 @@ public = [ [errors] classes = [ + "none", "encode_error", "parse_error", "validation_error", @@ -654,6 +663,249 @@ case_kinds = [ "food_availability.validate_revision.invalid", ] +[operations.phase1_publication_artifact_build_profile] +domain = "publication" +id = "publication_artifact.build_profile" +stability = "beta" +inputs = ["RadrootsAuthoredProfile", "u64", "String"] +outputs = ["RadrootsPhase1PublicationArtifact"] +error_class = "validation_error" +deterministic = true +signing = "none" +transport = "none" + +[operations.phase1_publication_artifact_build_profile.implementation] +rust_modules = [ + "crates/event/src/profile.rs", + "crates/event_codec/src/profile/authored.rs", + "crates/event_codec/src/wire/publication.rs", +] +rust_types = [ + "radroots_event_codec::wire::publication::RadrootsPhase1PublicationArtifact", + "radroots_event_codec::wire::publication::RadrootsPhase1PublicationArtifactError", + "radroots_event_codec::wire::publication::RadrootsPhase1PublicationSemanticVariant", +] + +[operations.phase1_publication_artifact_build_profile.conformance] +vector = "contracts/conformance/vectors/publication/phase1_artifact.v1.json" +case_kinds = ["publication_artifact.build_profile.valid"] + +[operations.phase1_publication_artifact_build_update] +domain = "publication" +id = "publication_artifact.build_update" +stability = "beta" +inputs = ["RadrootsAuthoredUpdate", "u64", "String"] +outputs = ["RadrootsPhase1PublicationArtifact"] +error_class = "validation_error" +deterministic = true +signing = "none" +transport = "none" + +[operations.phase1_publication_artifact_build_update.implementation] +rust_modules = [ + "crates/event/src/post.rs", + "crates/event_codec/src/post/authored.rs", + "crates/event_codec/src/wire/publication.rs", +] +rust_types = [ + "radroots_event_codec::wire::publication::RadrootsPhase1PublicationArtifact", + "radroots_event_codec::wire::publication::RadrootsPhase1PublicationArtifactError", + "radroots_event_codec::wire::publication::RadrootsPhase1PublicationSemanticVariant", +] + +[operations.phase1_publication_artifact_build_update.conformance] +vector = "contracts/conformance/vectors/publication/phase1_artifact.v1.json" +case_kinds = ["publication_artifact.build_update.valid"] + +[operations.phase1_publication_artifact_build_photo_update] +domain = "publication" +id = "publication_artifact.build_photo_update" +stability = "beta" +inputs = ["RadrootsAuthoredPhotoUpdate", "u64", "String"] +outputs = ["RadrootsPhase1PublicationArtifact"] +error_class = "validation_error" +deterministic = true +signing = "none" +transport = "none" + +[operations.phase1_publication_artifact_build_photo_update.implementation] +rust_modules = [ + "crates/event/src/post.rs", + "crates/event_codec/src/post/authored.rs", + "crates/event_codec/src/wire/publication.rs", +] +rust_types = [ + "radroots_event_codec::wire::publication::RadrootsPhase1PublicationArtifact", + "radroots_event_codec::wire::publication::RadrootsPhase1PublicationArtifactError", + "radroots_event_codec::wire::publication::RadrootsPhase1PublicationSemanticVariant", +] + +[operations.phase1_publication_artifact_build_photo_update.conformance] +vector = "contracts/conformance/vectors/publication/phase1_artifact.v1.json" +case_kinds = ["publication_artifact.build_photo_update.valid"] + +[operations.phase1_publication_artifact_build_ask] +domain = "publication" +id = "publication_artifact.build_ask" +stability = "beta" +inputs = ["RadrootsAuthoredAsk", "u64", "String"] +outputs = ["RadrootsPhase1PublicationArtifact"] +error_class = "validation_error" +deterministic = true +signing = "none" +transport = "none" + +[operations.phase1_publication_artifact_build_ask.implementation] +rust_modules = [ + "crates/event/src/post.rs", + "crates/event_codec/src/post/authored.rs", + "crates/event_codec/src/wire/publication.rs", +] +rust_types = [ + "radroots_event_codec::wire::publication::RadrootsPhase1PublicationArtifact", + "radroots_event_codec::wire::publication::RadrootsPhase1PublicationArtifactError", + "radroots_event_codec::wire::publication::RadrootsPhase1PublicationSemanticVariant", +] + +[operations.phase1_publication_artifact_build_ask.conformance] +vector = "contracts/conformance/vectors/publication/phase1_artifact.v1.json" +case_kinds = ["publication_artifact.build_ask.valid"] + +[operations.phase1_publication_artifact_build_calendar_date_event] +domain = "publication" +id = "publication_artifact.build_calendar_date_event" +stability = "beta" +inputs = ["RadrootsAuthoredCalendarDateEvent", "u64", "String"] +outputs = ["RadrootsPhase1PublicationArtifact"] +error_class = "validation_error" +deterministic = true +signing = "none" +transport = "none" + +[operations.phase1_publication_artifact_build_calendar_date_event.implementation] +rust_modules = [ + "crates/event/src/calendar.rs", + "crates/event_codec/src/calendar/encode.rs", + "crates/event_codec/src/wire/publication.rs", +] +rust_types = [ + "radroots_event_codec::wire::publication::RadrootsPhase1PublicationArtifact", + "radroots_event_codec::wire::publication::RadrootsPhase1PublicationArtifactError", + "radroots_event_codec::wire::publication::RadrootsPhase1PublicationSemanticVariant", +] + +[operations.phase1_publication_artifact_build_calendar_date_event.conformance] +vector = "contracts/conformance/vectors/publication/phase1_artifact.v1.json" +case_kinds = ["publication_artifact.build_calendar_date_event.valid"] + +[operations.phase1_publication_artifact_build_calendar_time_event] +domain = "publication" +id = "publication_artifact.build_calendar_time_event" +stability = "beta" +inputs = ["RadrootsAuthoredCalendarTimeEvent", "u64", "String"] +outputs = ["RadrootsPhase1PublicationArtifact"] +error_class = "validation_error" +deterministic = true +signing = "none" +transport = "none" + +[operations.phase1_publication_artifact_build_calendar_time_event.implementation] +rust_modules = [ + "crates/event/src/calendar.rs", + "crates/event_codec/src/calendar/encode.rs", + "crates/event_codec/src/wire/publication.rs", +] +rust_types = [ + "radroots_event_codec::wire::publication::RadrootsPhase1PublicationArtifact", + "radroots_event_codec::wire::publication::RadrootsPhase1PublicationArtifactError", + "radroots_event_codec::wire::publication::RadrootsPhase1PublicationSemanticVariant", +] + +[operations.phase1_publication_artifact_build_calendar_time_event.conformance] +vector = "contracts/conformance/vectors/publication/phase1_artifact.v1.json" +case_kinds = ["publication_artifact.build_calendar_time_event.valid"] + +[operations.phase1_publication_artifact_build_food_availability] +domain = "publication" +id = "publication_artifact.build_food_availability" +stability = "beta" +inputs = ["RadrootsFoodAvailabilityDetails", "u64", "String"] +outputs = ["RadrootsPhase1PublicationArtifact"] +error_class = "validation_error" +deterministic = true +signing = "none" +transport = "none" + +[operations.phase1_publication_artifact_build_food_availability.implementation] +rust_modules = [ + "crates/event/src/food_availability.rs", + "crates/event_codec/src/food_availability/authored.rs", + "crates/event_codec/src/wire/publication.rs", +] +rust_types = [ + "radroots_event_codec::wire::publication::RadrootsPhase1PublicationArtifact", + "radroots_event_codec::wire::publication::RadrootsPhase1PublicationArtifactError", + "radroots_event_codec::wire::publication::RadrootsPhase1PublicationSemanticVariant", +] + +[operations.phase1_publication_artifact_build_food_availability.conformance] +vector = "contracts/conformance/vectors/publication/phase1_artifact.v1.json" +case_kinds = ["publication_artifact.build_food_availability.valid"] + +[operations.phase1_publication_artifact_to_canonical_json] +domain = "publication" +id = "publication_artifact.to_canonical_json" +stability = "beta" +inputs = ["RadrootsPhase1PublicationArtifact"] +outputs = ["Bytes"] +error_class = "none" +deterministic = true +signing = "none" +transport = "none" + +[operations.phase1_publication_artifact_to_canonical_json.implementation] +rust_modules = ["crates/event_codec/src/wire/publication.rs"] +rust_types = [ + "radroots_event_codec::wire::publication::RadrootsPhase1PublicationArtifact", +] + +[operations.phase1_publication_artifact_to_canonical_json.conformance] +vector = "contracts/conformance/vectors/publication/phase1_artifact.v1.json" +case_kinds = ["publication_artifact.to_canonical_json.valid"] + +[operations.phase1_publication_artifact_from_canonical_json] +domain = "publication" +id = "publication_artifact.from_canonical_json" +stability = "beta" +inputs = ["Bytes"] +outputs = ["RadrootsPhase1PublicationArtifact"] +error_class = "parse_error" +deterministic = true +signing = "none" +transport = "none" + +[operations.phase1_publication_artifact_from_canonical_json.implementation] +rust_modules = [ + "crates/event_codec/src/calendar/decode.rs", + "crates/event_codec/src/food_availability/inbound.rs", + "crates/event_codec/src/food_availability/inbound/registry_v7.rs", + "crates/event_codec/src/post/authored.rs", + "crates/event_codec/src/post/inbound.rs", + "crates/event_codec/src/post/inbound/registry_v7.rs", + "crates/event_codec/src/wire/publication.rs", +] +rust_types = [ + "radroots_event_codec::wire::publication::RadrootsPhase1PublicationArtifact", + "radroots_event_codec::wire::publication::RadrootsPhase1PublicationArtifactError", +] + +[operations.phase1_publication_artifact_from_canonical_json.conformance] +vector = "contracts/conformance/vectors/publication/phase1_artifact.v1.json" +case_kinds = [ + "publication_artifact.from_canonical_json.valid", + "publication_artifact.from_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 @@ -277,6 +277,17 @@ semver_impacts = [ summary = "Add one signature-verified event admission operation that preserves typed product admissions, applies complete registry validation to generic contracts, and distinguishes unsupported matching, invalid shapes, Post-to-Reply routing, and kind-30402 profile exclusions." [[changes]] +id = "phase1-publication-artifact" +classification = "feature" +semver_impacts = [ + "add_exported_type", + "add_exported_function", + "add_exported_constant", + "add_conformance_vector", +] +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 = "event-store-validity-visibility-split" classification = "breaking" semver_impacts = [ diff --git a/crates/event_codec/Cargo.toml b/crates/event_codec/Cargo.toml @@ -16,7 +16,7 @@ readme = "README" default = ["std"] std = ["radroots_blossom/std", "radroots_core/std", "radroots_event/std"] serde = ["dep:serde", "radroots_core/serde", "radroots_event/serde"] -serde_json = ["serde", "dep:serde_json"] +serde_json = ["serde", "dep:hex", "dep:serde_json", "dep:sha2"] nostr = ["dep:nostr", "std"] knowledge = ["serde_json", "radroots_event/knowledge"] knowledge-nip54 = ["knowledge", "radroots_event/knowledge-nip54"] diff --git a/crates/event_codec/README b/crates/event_codec/README @@ -1,5 +1,28 @@ # radroots_event_codec +The optional `serde_json` feature includes the sealed Phase 1 publication +artifact. It accepts only the strict authored Profile, Update, PhotoUpdate, +Ask, date/time Event, and FoodAvailability models. The seven serialized leaves +are `profile`, `update`, `photo_update`, `ask`, `event_date`, `event_time`, and +`food_availability`; Event remains one semantic role with typed date and time +subvariants. + +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`, +`event_contract_id`, `expected_author`, `draft`, `expected_event_id`, +`media_references`, and `artifact_digest`. Compact JSON is capped at 2,097,152 +bytes, the eventual signed-event wire at 262,144 bytes, and media commitments +at 4,096 complete canonical URLs. Primary byte-verified BUD-02 descriptor URLs +retain their required hash-path extension; post fallbacks may be extensionless. + +The artifact digest is SHA-256 over the ASCII domain +`radroots.phase1.publication-artifact.v1`, one literal NUL byte, and the exact +canonical envelope through `media_references`. Reloading rejects alternate +encodings, noncanonical identifiers or URL casing, profile drift, and digest +tampering, but does not restore byte verification, upload, signing, or +authenticity capabilities. + This is the README for `radroots_event_codec`, which provides canonical event codecs and tag builders for the `radroots` core libraries. diff --git a/crates/event_codec/contracts/phase1_publication_artifact_v1.manifest.json b/crates/event_codec/contracts/phase1_publication_artifact_v1.manifest.json @@ -0,0 +1,2387 @@ +{ + "schema_version": 1, + "contract_id": "radroots_event_codec.phase1_publication_artifact_v1", + "authority_id": "phase1_publication_artifact_v1", + "manifest_schema": { + "path": "crates/event_codec/contracts/phase1_publication_artifact_v1.manifest.schema.json", + "byte_length": 11972, + "sha256": "1d72cee2754e7ac45105d79b1ecf7d44251991be7a18ba106166e962000e8320", + "hash_algorithm": "sha256_bytes_v1" + }, + "predecessor": { + "contract_id": "radroots_event_store.raw_source_rebuild_v1", + "manifest": { + "path": "crates/event_store/contracts/raw_source_rebuild_v1.manifest.json", + "byte_length": 45449, + "sha256": "03253ce31dc31d465880a895d2685f5deb1274948e0a4eabe81a2f08f238c483", + "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_publication_authority_v1" + }, + "artifact": { + "schema_version": 1, + "validator": "validate_phase1_publication_artifact", + "semantic_roles": [ + "profile", + "update", + "photo_update", + "ask", + "event", + "food_availability" + ], + "serialized_semantic_variants": [ + "profile", + "update", + "photo_update", + "ask", + "event_date", + "event_time", + "food_availability" + ], + "event_subvariants": [ + "date", + "time" + ], + "canonical_encoding": "serde_json_compact_struct_field_order_v1", + "artifact_max_bytes": 2097152, + "signed_event_wire_max_bytes": 262144, + "media_reference_max_count": 4096, + "envelope_fields": [ + "schema_version", + "semantic_variant", + "authored_operation_id", + "event_contract_id", + "expected_author", + "draft", + "expected_event_id", + "media_references", + "artifact_digest" + ], + "draft_fields": [ + "created_at", + "kind", + "tags", + "content" + ], + "media_reference_fields": [ + "url", + "sha256", + "size", + "media_type" + ], + "media_reference_identity": "exact_case_preserving_approved_url_with_descriptor_commitment_v1", + "primary_media_url_requirement": "blossom_hash_path_extension_required_v1", + "post_fallback_url_requirement": "approved_blossom_url_extension_optional_v1", + "digest_algorithm": "sha256_domain_nul_canonical_json_v1", + "digest_domain": "radroots.phase1.publication-artifact.v1", + "digest_domain_terminator": "0x00", + "digest_preimage": "ascii_domain_then_single_nul_then_canonical_envelope_without_digest_v1", + "constructors": [ + { + "semantic_variant": "profile", + "serialized_semantic_variant": "profile", + "event_variant": null, + "strict_input": "RadrootsAuthoredProfile", + "constructor": "RadrootsPhase1PublicationArtifact::from_profile", + "publication_operation_id": "publication_artifact.build_profile", + "authored_operation_id": "profile.build_authored_draft", + "event_contract_id": "radroots.profile.metadata.v1", + "kind": 0 + }, + { + "semantic_variant": "update", + "serialized_semantic_variant": "update", + "event_variant": null, + "strict_input": "RadrootsAuthoredUpdate", + "constructor": "RadrootsPhase1PublicationArtifact::from_update", + "publication_operation_id": "publication_artifact.build_update", + "authored_operation_id": "social.update.build_authored_draft", + "event_contract_id": "radroots.social.update.v1", + "kind": 1 + }, + { + "semantic_variant": "photo_update", + "serialized_semantic_variant": "photo_update", + "event_variant": null, + "strict_input": "RadrootsAuthoredPhotoUpdate", + "constructor": "RadrootsPhase1PublicationArtifact::from_photo_update", + "publication_operation_id": "publication_artifact.build_photo_update", + "authored_operation_id": "social.photo_update.build_authored_draft", + "event_contract_id": "radroots.social.photo_update.v1", + "kind": 1 + }, + { + "semantic_variant": "ask", + "serialized_semantic_variant": "ask", + "event_variant": null, + "strict_input": "RadrootsAuthoredAsk", + "constructor": "RadrootsPhase1PublicationArtifact::from_ask", + "publication_operation_id": "publication_artifact.build_ask", + "authored_operation_id": "social.ask.build_authored_draft", + "event_contract_id": "radroots.social.ask.v1", + "kind": 1 + }, + { + "semantic_variant": "event", + "serialized_semantic_variant": "event_date", + "event_variant": "date", + "strict_input": "RadrootsAuthoredCalendarDateEvent", + "constructor": "RadrootsPhase1PublicationArtifact::from_calendar_date_event", + "publication_operation_id": "publication_artifact.build_calendar_date_event", + "authored_operation_id": "social.calendar_date_event.build_authored_draft", + "event_contract_id": "radroots.calendar.date_event.v1", + "kind": 31922 + }, + { + "semantic_variant": "event", + "serialized_semantic_variant": "event_time", + "event_variant": "time", + "strict_input": "RadrootsAuthoredCalendarTimeEvent", + "constructor": "RadrootsPhase1PublicationArtifact::from_calendar_time_event", + "publication_operation_id": "publication_artifact.build_calendar_time_event", + "authored_operation_id": "social.calendar_time_event.build_authored_draft", + "event_contract_id": "radroots.calendar.time_event.v1", + "kind": 31923 + }, + { + "semantic_variant": "food_availability", + "serialized_semantic_variant": "food_availability", + "event_variant": null, + "strict_input": "RadrootsFoodAvailabilityDetails", + "constructor": "RadrootsPhase1PublicationArtifact::from_food_availability", + "publication_operation_id": "publication_artifact.build_food_availability", + "authored_operation_id": "food_availability.build_authored_draft", + "event_contract_id": "radroots.food.availability.v1", + "kind": 30402 + } + ], + "denied_inputs": [ + "arbitrary_event_draft", + "raw_json", + "numeric_kind", + "signed_event", + "private_key", + "signer" + ], + "reload_capability": "persisted_artifact_only_no_prior_capability_restoration_v1", + "threat_boundary": [ + "detects_accidental_corruption", + "detects_payload_only_modification", + "detects_digest_only_modification", + "does_not_authenticate_actor_rewriting_payload_and_digest", + "does_not_survive_validator_binary_or_host_compromise", + "does_not_restore_byte_verification_or_upload_completion", + "does_not_replace_nip01_id_and_signature_verification" + ] + }, + "operations": [ + { + "id": "publication_artifact.build_profile", + "strict_input": "RadrootsAuthoredProfile", + "output": "RadrootsPhase1PublicationArtifact", + "error_class": "validation_error", + "signing": "none", + "transport": "none" + }, + { + "id": "publication_artifact.build_update", + "strict_input": "RadrootsAuthoredUpdate", + "output": "RadrootsPhase1PublicationArtifact", + "error_class": "validation_error", + "signing": "none", + "transport": "none" + }, + { + "id": "publication_artifact.build_photo_update", + "strict_input": "RadrootsAuthoredPhotoUpdate", + "output": "RadrootsPhase1PublicationArtifact", + "error_class": "validation_error", + "signing": "none", + "transport": "none" + }, + { + "id": "publication_artifact.build_ask", + "strict_input": "RadrootsAuthoredAsk", + "output": "RadrootsPhase1PublicationArtifact", + "error_class": "validation_error", + "signing": "none", + "transport": "none" + }, + { + "id": "publication_artifact.build_calendar_date_event", + "strict_input": "RadrootsAuthoredCalendarDateEvent", + "output": "RadrootsPhase1PublicationArtifact", + "error_class": "validation_error", + "signing": "none", + "transport": "none" + }, + { + "id": "publication_artifact.build_calendar_time_event", + "strict_input": "RadrootsAuthoredCalendarTimeEvent", + "output": "RadrootsPhase1PublicationArtifact", + "error_class": "validation_error", + "signing": "none", + "transport": "none" + }, + { + "id": "publication_artifact.build_food_availability", + "strict_input": "RadrootsFoodAvailabilityDetails", + "output": "RadrootsPhase1PublicationArtifact", + "error_class": "validation_error", + "signing": "none", + "transport": "none" + }, + { + "id": "publication_artifact.to_canonical_json", + "strict_input": "RadrootsPhase1PublicationArtifact", + "output": "Bytes", + "error_class": "none", + "signing": "none", + "transport": "none" + }, + { + "id": "publication_artifact.from_canonical_json", + "strict_input": "Bytes", + "output": "RadrootsPhase1PublicationArtifact", + "error_class": "parse_error", + "signing": "none", + "transport": "none" + } + ], + "predecessor_source_supersessions": [ + "CHANGELOG.md", + "contracts/releases/1.0.0-alpha.1.toml", + "tools/xtask/src/contract.rs", + "tools/xtask/src/main.rs", + "tools/xtask/src/contract/nip09_reconciliation.rs", + "tools/xtask/src/contract/raw_source_rebuild.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": 30404, + "sha256": "a66f398154e7a48a9cef24630db14e4bc659909c929f5e03318c2fc97656fbb9", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "workspace_lockfile_authority", + "path": "Cargo.lock", + "byte_length": 216944, + "sha256": "d827b8c5aceb550b62fcc9b98804f97bbaf6bca95a59b1011a9927ba5aa2e49f", + "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": 75850, + "sha256": "c65893a44ee7ecee0617764f84b08018141e7e249cfdfa9c8144548c7371f510", + "hash_algorithm": "sha256_bytes_v1" + }, + { + "role": "release_authority", + "path": "contracts/releases/1.0.0-alpha.1.toml", + "byte_length": 21010, + "sha256": "1b2f341fdd6827a7a73642de519b26b2aeedb0b9eb542f16a67c962e18dcc721", + "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": 21360, + "sha256": "ed2bda152f594c3c9b751399870e0738c5debb12c63103e94862182c1bccf3a8", + "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": 59609, + "sha256": "18046b34c831c56ceee0a21a65c48072a7b05de41f0087095ed6f0354335170e", + "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": 480750, + "sha256": "45268a8f22249d3419581e23252a39205e73c53f32270a5dd41bf07c9552a31b", + "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_contract_governance", + "path": "tools/xtask/src/contract/phase1_publication_artifact.rs", + "byte_length": 69965, + "sha256": "d5a013cb47159bd0c35e8f3a7bb86999d81fadc8bd256bf4c4b419b60a723da0", + "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": 16040, + "sha256": "53c81bef2c70a0d3b4bdfa6baa44c3dc49d3601da2d094d8d66df1d6e4a39ad2", + "hash_algorithm": "sha256_bytes_v1" + } + ], + "result_vector": { + "canonical_path": "contracts/conformance/vectors/publication/phase1_artifact.v1.json", + "mirror_path": "crates/event_codec/tests/fixtures/phase1_publication_artifact.v1.json", + "byte_length": 23113, + "sha256": "ec18c687d5b0710a48624ddb620d89157e6b645dbea8bb91c62e3a111d20c622", + "hash_algorithm": "sha256_bytes_v1", + "executor_path": "crates/event_codec/tests/publication_artifact.rs", + "executor_test": "publication_artifact_conformance_vector_executes_every_case", + "valid_case_ids": [ + "profile_round_trip", + "update_round_trip", + "photo_update_round_trip", + "ask_round_trip_with_fallback", + "event_date_round_trip", + "event_time_round_trip", + "food_availability_round_trip", + "canonical_json_serialization", + "canonical_json_reload" + ], + "invalid_case_ids": [ + "leading_whitespace_is_noncanonical", + "artifact_exact_byte_limit_reaches_parser", + "artifact_one_byte_over_limit_is_rejected", + "unknown_field_is_rejected", + "unknown_draft_field_is_rejected", + "nested_expected_event_id_is_rejected", + "missing_expected_event_id_is_rejected", + "malformed_expected_event_id_is_rejected", + "uppercase_expected_event_id_is_rejected", + "duplicate_expected_event_id_is_rejected", + "unknown_media_field_is_rejected", + "json_field_order_is_noncanonical", + "unknown_version_is_rejected", + "cross_variant_is_rejected", + "operation_mismatch_is_rejected", + "contract_mismatch_is_rejected", + "kind_mismatch_is_rejected", + "author_mismatch_is_rejected", + "created_at_tamper_is_rejected", + "draft_tags_tamper_is_rejected", + "draft_content_tamper_is_rejected", + "noncanonical_nip05_is_rejected_after_id_rebuild", + "empty_ask_content_is_rejected_after_id_rebuild", + "expected_event_id_tamper_is_rejected", + "digest_tamper_is_rejected", + "media_order_is_rejected", + "profile_media_commitment_tamper_is_rejected", + "media_url_tamper_is_rejected", + "noncanonical_media_url_casing_is_rejected", + "media_hash_tamper_is_rejected", + "media_type_tamper_is_rejected", + "post_media_commitment_must_match_imeta" + ] + }, + "release": { + "record_path": "contracts/releases/1.0.0-alpha.1.toml", + "change_id": "phase1-publication-artifact", + "changelog_path": "CHANGELOG.md", + "changelog_marker": "<!-- release-change: phase1-publication-artifact -->" + } +} diff --git a/crates/event_codec/contracts/phase1_publication_artifact_v1.manifest.schema.json b/crates/event_codec/contracts/phase1_publication_artifact_v1.manifest.schema.json @@ -0,0 +1,493 @@ +{ + "$defs": { + "constructor": { + "additionalProperties": false, + "properties": { + "authored_operation_id": { + "minLength": 1, + "type": "string" + }, + "constructor": { + "minLength": 1, + "type": "string" + }, + "event_contract_id": { + "minLength": 1, + "type": "string" + }, + "event_variant": { + "type": [ + "string", + "null" + ] + }, + "kind": { + "minimum": 0, + "type": "integer" + }, + "publication_operation_id": { + "minLength": 1, + "type": "string" + }, + "semantic_variant": { + "minLength": 1, + "type": "string" + }, + "serialized_semantic_variant": { + "minLength": 1, + "type": "string" + }, + "strict_input": { + "minLength": 1, + "type": "string" + } + }, + "required": [ + "semantic_variant", + "serialized_semantic_variant", + "event_variant", + "strict_input", + "constructor", + "publication_operation_id", + "authored_operation_id", + "event_contract_id", + "kind" + ], + "type": "object" + }, + "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" + }, + "operation": { + "additionalProperties": false, + "properties": { + "error_class": { + "enum": [ + "none", + "validation_error", + "parse_error" + ] + }, + "id": { + "minLength": 1, + "type": "string" + }, + "output": { + "enum": [ + "RadrootsPhase1PublicationArtifact", + "Bytes" + ] + }, + "signing": { + "const": "none" + }, + "strict_input": { + "minLength": 1, + "type": "string" + }, + "transport": { + "const": "none" + } + }, + "required": [ + "id", + "strict_input", + "output", + "error_class", + "signing", + "transport" + ], + "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-artifact-v1.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false, + "properties": { + "artifact": { + "additionalProperties": false, + "properties": { + "artifact_max_bytes": { + "const": 2097152 + }, + "canonical_encoding": { + "const": "serde_json_compact_struct_field_order_v1" + }, + "constructors": { + "items": { + "$ref": "#/$defs/constructor" + }, + "maxItems": 7, + "minItems": 7, + "type": "array" + }, + "denied_inputs": { + "const": [ + "arbitrary_event_draft", + "raw_json", + "numeric_kind", + "signed_event", + "private_key", + "signer" + ] + }, + "digest_algorithm": { + "const": "sha256_domain_nul_canonical_json_v1" + }, + "digest_domain": { + "const": "radroots.phase1.publication-artifact.v1" + }, + "digest_domain_terminator": { + "const": "0x00" + }, + "digest_preimage": { + "const": "ascii_domain_then_single_nul_then_canonical_envelope_without_digest_v1" + }, + "draft_fields": { + "const": [ + "created_at", + "kind", + "tags", + "content" + ] + }, + "envelope_fields": { + "const": [ + "schema_version", + "semantic_variant", + "authored_operation_id", + "event_contract_id", + "expected_author", + "draft", + "expected_event_id", + "media_references", + "artifact_digest" + ] + }, + "event_subvariants": { + "const": [ + "date", + "time" + ] + }, + "media_reference_fields": { + "const": [ + "url", + "sha256", + "size", + "media_type" + ] + }, + "media_reference_identity": { + "const": "exact_case_preserving_approved_url_with_descriptor_commitment_v1" + }, + "media_reference_max_count": { + "const": 4096 + }, + "post_fallback_url_requirement": { + "const": "approved_blossom_url_extension_optional_v1" + }, + "primary_media_url_requirement": { + "const": "blossom_hash_path_extension_required_v1" + }, + "reload_capability": { + "const": "persisted_artifact_only_no_prior_capability_restoration_v1" + }, + "schema_version": { + "const": 1 + }, + "semantic_roles": { + "const": [ + "profile", + "update", + "photo_update", + "ask", + "event", + "food_availability" + ] + }, + "serialized_semantic_variants": { + "const": [ + "profile", + "update", + "photo_update", + "ask", + "event_date", + "event_time", + "food_availability" + ] + }, + "signed_event_wire_max_bytes": { + "const": 262144 + }, + "threat_boundary": { + "const": [ + "detects_accidental_corruption", + "detects_payload_only_modification", + "detects_digest_only_modification", + "does_not_authenticate_actor_rewriting_payload_and_digest", + "does_not_survive_validator_binary_or_host_compromise", + "does_not_restore_byte_verification_or_upload_completion", + "does_not_replace_nip01_id_and_signature_verification" + ] + }, + "validator": { + "const": "validate_phase1_publication_artifact" + } + }, + "required": [ + "schema_version", + "validator", + "semantic_roles", + "serialized_semantic_variants", + "event_subvariants", + "canonical_encoding", + "artifact_max_bytes", + "signed_event_wire_max_bytes", + "media_reference_max_count", + "envelope_fields", + "draft_fields", + "media_reference_fields", + "media_reference_identity", + "primary_media_url_requirement", + "post_fallback_url_requirement", + "digest_algorithm", + "digest_domain", + "digest_domain_terminator", + "digest_preimage", + "constructors", + "denied_inputs", + "reload_capability", + "threat_boundary" + ], + "type": "object" + }, + "authority_id": { + "const": "phase1_publication_artifact_v1" + }, + "contract_id": { + "const": "radroots_event_codec.phase1_publication_artifact_v1" + }, + "event_contract_registry": { + "additionalProperties": false, + "properties": { + "evolution": { + "minLength": 1, + "type": "string" + }, + "inventory": { + "$ref": "#/$defs/file" + }, + "sidecar": { + "$ref": "#/$defs/file" + }, + "version": { + "const": 7 + } + }, + "required": [ + "version", + "inventory", + "sidecar", + "evolution" + ], + "type": "object" + }, + "manifest_schema": { + "$ref": "#/$defs/file" + }, + "operations": { + "items": { + "$ref": "#/$defs/operation" + }, + "maxItems": 9, + "minItems": 9, + "type": "array" + }, + "predecessor": { + "additionalProperties": false, + "properties": { + "contract_id": { + "const": "radroots_event_store.raw_source_rebuild_v1" + }, + "manifest": { + "$ref": "#/$defs/file" + } + }, + "required": [ + "contract_id", + "manifest" + ], + "type": "object" + }, + "predecessor_source_supersessions": { + "items": { + "minLength": 1, + "type": "string" + }, + "maxItems": 6, + "minItems": 6, + "type": "array" + }, + "release": { + "additionalProperties": false, + "properties": { + "change_id": { + "const": "phase1-publication-artifact" + }, + "changelog_marker": { + "const": "<!-- release-change: phase1-publication-artifact -->" + }, + "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_artifact.v1.json" + }, + "executor_path": { + "const": "crates/event_codec/tests/publication_artifact.rs" + }, + "executor_test": { + "const": "publication_artifact_conformance_vector_executes_every_case" + }, + "hash_algorithm": { + "const": "sha256_bytes_v1" + }, + "invalid_case_ids": { + "items": { + "type": "string" + }, + "minItems": 24, + "type": "array" + }, + "mirror_path": { + "const": "crates/event_codec/tests/fixtures/phase1_publication_artifact.v1.json" + }, + "sha256": { + "pattern": "^[0-9a-f]{64}$", + "type": "string" + }, + "valid_case_ids": { + "items": { + "type": "string" + }, + "minItems": 7, + "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", + "artifact", + "operations", + "predecessor_source_supersessions", + "source_files", + "result_vector", + "release" + ], + "title": "Radroots Phase 1 Publication Artifact Contract", + "type": "object" +} diff --git a/crates/event_codec/contracts/phase1_publication_artifact_v1.manifest.sha256 b/crates/event_codec/contracts/phase1_publication_artifact_v1.manifest.sha256 @@ -0,0 +1 @@ +a07aace74f4747ba6e769a99acad7eadaac2d19d26aa0dd1c280ab92454519b5 diff --git a/crates/event_codec/src/lib.rs b/crates/event_codec/src/lib.rs @@ -1,4 +1,4 @@ -#![cfg_attr(not(feature = "std"), no_std)] +#![cfg_attr(all(not(feature = "std"), not(test)), no_std)] #![cfg_attr(coverage_nightly, feature(coverage_attribute))] #![forbid(unsafe_code)] #[cfg(not(feature = "std"))] diff --git a/crates/event_codec/src/wire.rs b/crates/event_codec/src/wire.rs @@ -4,6 +4,9 @@ use alloc::{ vec::Vec, }; +#[cfg(feature = "serde_json")] +pub mod publication; + pub fn canonicalize_tags(tags: &mut Vec<Vec<String>>) { tags.retain(|t| t.first().map(|s| !s.trim().is_empty()).unwrap_or(false)); for t in tags.iter_mut() { diff --git a/crates/event_codec/src/wire/publication.rs b/crates/event_codec/src/wire/publication.rs @@ -0,0 +1,1596 @@ +//! Sealed persistence boundary for the Radroots Phase 1 publication set. +//! +//! Artifacts can only be created from strict authored models or reloaded from +//! their exact canonical JSON bytes. Reloading restores a persisted artifact; +//! it does not restore byte-verification, upload, signing, or authenticity +//! capabilities. +//! +//! The digest detects accidental corruption, payload-only modification, and +//! digest-only modification. It does not authenticate against an actor able to +//! rewrite both the payload and digest, replace this validator, compromise the +//! signer, or control the host/database. NIP-01 id and signature verification +//! remain the cryptographic authenticity boundary. + +#[cfg(not(feature = "std"))] +use alloc::{ + format, + string::{String, ToString}, + vec, + vec::Vec, +}; +use core::fmt; + +use radroots_blossom::{ + RadrootsBlossomApprovedBlobUrl, RadrootsBlossomByteVerifiedDescriptor, + RadrootsBlossomMediaType, RadrootsBlossomSha256, url::RadrootsBlossomBlobUrl, +}; +use radroots_event::{ + RadrootsEventTags, + calendar::{ + RadrootsAdmittedCalendarDateEvent, RadrootsAdmittedCalendarTimeEvent, + RadrootsAuthoredCalendarDateEvent, RadrootsAuthoredCalendarTimeEvent, + RadrootsParsedNip52CalendarCommon, + }, + food_availability::{RADROOTS_FOOD_AVAILABILITY_CONTRACT_ID, RadrootsFoodAvailabilityDetails}, + ids::{RadrootsEventId, RadrootsPublicKey}, + kinds::{ + KIND_CALENDAR_DATE_EVENT, KIND_CALENDAR_TIME_EVENT, KIND_CLASSIFIED_LISTING, KIND_POST, + KIND_PROFILE, + }, + post::{RadrootsAuthoredAsk, RadrootsAuthoredPhotoUpdate, RadrootsAuthoredUpdate}, + profile::{ + RADROOTS_PROFILE_METADATA_MAX_CONTENT_BYTES, RadrootsAuthoredProfile, + RadrootsNip05Identifier, + }, + wire::{ + DEFAULT_CONTENT_MAX_BYTES, DEFAULT_RAW_JSON_MAX_BYTES, RadrootsNip01EventWireParts, + compute_canonical_nip01_event_id, + }, +}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +use crate::{ + calendar::{decode, encode}, + food_availability::{authored as food_authored, inbound as food_inbound}, + post::{authored as post_authored, inbound as post_inbound}, + profile::authored as profile_authored, +}; + +pub const RADROOTS_PHASE1_PUBLICATION_ARTIFACT_SCHEMA_VERSION: u32 = 1; +pub const RADROOTS_PHASE1_PUBLICATION_ARTIFACT_MAX_BYTES: usize = 2 * 1024 * 1024; +pub const RADROOTS_PHASE1_PUBLICATION_MEDIA_MAX_COUNT: usize = 4096; +pub const RADROOTS_PHASE1_PUBLICATION_SIGNED_EVENT_MAX_BYTES: usize = DEFAULT_RAW_JSON_MAX_BYTES; + +const ARTIFACT_DIGEST_DOMAIN: &[u8] = b"radroots.phase1.publication-artifact.v1"; +const ARTIFACT_DIGEST_DOMAIN_TERMINATOR: &[u8] = b"\0"; +const PROFILE_OPERATION_ID: &str = "profile.build_authored_draft"; +const PROFILE_CONTRACT_ID: &str = "radroots.profile.metadata.v1"; +const UPDATE_OPERATION_ID: &str = "social.update.build_authored_draft"; +const UPDATE_CONTRACT_ID: &str = "radroots.social.update.v1"; +const PHOTO_UPDATE_OPERATION_ID: &str = "social.photo_update.build_authored_draft"; +const PHOTO_UPDATE_CONTRACT_ID: &str = "radroots.social.photo_update.v1"; +const ASK_OPERATION_ID: &str = "social.ask.build_authored_draft"; +const ASK_CONTRACT_ID: &str = "radroots.social.ask.v1"; +const CALENDAR_DATE_OPERATION_ID: &str = "social.calendar_date_event.build_authored_draft"; +const CALENDAR_DATE_CONTRACT_ID: &str = "radroots.calendar.date_event.v1"; +const CALENDAR_TIME_OPERATION_ID: &str = "social.calendar_time_event.build_authored_draft"; +const CALENDAR_TIME_CONTRACT_ID: &str = "radroots.calendar.time_event.v1"; +const FOOD_OPERATION_ID: &str = "food_availability.build_authored_draft"; + +/// The two strict NIP-52 event forms admitted by the Phase 1 Event root. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RadrootsPhase1PublicationEventVariant { + Date, + Time, +} + +/// Closed semantic set represented by a Phase 1 publication artifact. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RadrootsPhase1PublicationSemanticVariant { + Profile, + Update, + PhotoUpdate, + Ask, + Event(RadrootsPhase1PublicationEventVariant), + FoodAvailability, +} + +impl RadrootsPhase1PublicationSemanticVariant { + pub const fn as_str(self) -> &'static str { + match self { + Self::Profile => "profile", + Self::Update => "update", + Self::PhotoUpdate => "photo_update", + Self::Ask => "ask", + Self::Event(RadrootsPhase1PublicationEventVariant::Date) => "event_date", + Self::Event(RadrootsPhase1PublicationEventVariant::Time) => "event_time", + Self::FoodAvailability => "food_availability", + } + } + + fn parse(value: &str) -> Result<Self, RadrootsPhase1PublicationArtifactError> { + match value { + "profile" => Ok(Self::Profile), + "update" => Ok(Self::Update), + "photo_update" => Ok(Self::PhotoUpdate), + "ask" => Ok(Self::Ask), + "event_date" => Ok(Self::Event(RadrootsPhase1PublicationEventVariant::Date)), + "event_time" => Ok(Self::Event(RadrootsPhase1PublicationEventVariant::Time)), + "food_availability" => Ok(Self::FoodAvailability), + _ => Err(RadrootsPhase1PublicationArtifactError::UnknownSemanticVariant), + } + } + + pub const fn authored_operation_id(self) -> &'static str { + profile_for_variant(self).operation_id + } + + pub const fn event_contract_id(self) -> &'static str { + profile_for_variant(self).contract_id + } + + pub const fn kind(self) -> u32 { + profile_for_variant(self).kind + } +} + +#[derive(Clone, Copy)] +struct PublicationProfile { + operation_id: &'static str, + contract_id: &'static str, + kind: u32, +} + +const fn profile_for_variant( + variant: RadrootsPhase1PublicationSemanticVariant, +) -> PublicationProfile { + match variant { + RadrootsPhase1PublicationSemanticVariant::Profile => PublicationProfile { + operation_id: PROFILE_OPERATION_ID, + contract_id: PROFILE_CONTRACT_ID, + kind: KIND_PROFILE, + }, + RadrootsPhase1PublicationSemanticVariant::Update => PublicationProfile { + operation_id: UPDATE_OPERATION_ID, + contract_id: UPDATE_CONTRACT_ID, + kind: KIND_POST, + }, + RadrootsPhase1PublicationSemanticVariant::PhotoUpdate => PublicationProfile { + operation_id: PHOTO_UPDATE_OPERATION_ID, + contract_id: PHOTO_UPDATE_CONTRACT_ID, + kind: KIND_POST, + }, + RadrootsPhase1PublicationSemanticVariant::Ask => PublicationProfile { + operation_id: ASK_OPERATION_ID, + contract_id: ASK_CONTRACT_ID, + kind: KIND_POST, + }, + RadrootsPhase1PublicationSemanticVariant::Event( + RadrootsPhase1PublicationEventVariant::Date, + ) => PublicationProfile { + operation_id: CALENDAR_DATE_OPERATION_ID, + contract_id: CALENDAR_DATE_CONTRACT_ID, + kind: KIND_CALENDAR_DATE_EVENT, + }, + RadrootsPhase1PublicationSemanticVariant::Event( + RadrootsPhase1PublicationEventVariant::Time, + ) => PublicationProfile { + operation_id: CALENDAR_TIME_OPERATION_ID, + contract_id: CALENDAR_TIME_CONTRACT_ID, + kind: KIND_CALENDAR_TIME_EVENT, + }, + RadrootsPhase1PublicationSemanticVariant::FoodAvailability => PublicationProfile { + operation_id: FOOD_OPERATION_ID, + contract_id: RADROOTS_FOOD_AVAILABILITY_CONTRACT_ID, + kind: KIND_CLASSIFIED_LISTING, + }, + } +} + +/// Exact unsigned NIP-01 state frozen into a publication artifact. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsPhase1PublicationDraft { + created_at: u64, + kind: u32, + tags: Vec<Vec<String>>, + content: String, +} + +impl RadrootsPhase1PublicationDraft { + pub const fn created_at(&self) -> u64 { + self.created_at + } + + pub const fn kind(&self) -> u32 { + self.kind + } + + pub fn tags(&self) -> &[Vec<String>] { + &self.tags + } + + pub fn content(&self) -> &str { + &self.content + } +} + +/// Persisted media commitment associated with one complete canonical URL. +/// +/// This type preserves descriptor facts that entered through a byte-verified +/// authored model. Reloading it does not re-establish byte verification or +/// prove that the URL was uploaded or remains retrievable. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsPhase1PublicationMediaReference { + url: RadrootsBlossomApprovedBlobUrl, + sha256: RadrootsBlossomSha256, + size: u64, + media_type: RadrootsBlossomMediaType, +} + +impl RadrootsPhase1PublicationMediaReference { + pub fn url(&self) -> &RadrootsBlossomApprovedBlobUrl { + &self.url + } + + pub const fn sha256(&self) -> RadrootsBlossomSha256 { + self.sha256 + } + + pub const fn size(&self) -> u64 { + self.size + } + + pub fn media_type(&self) -> &RadrootsBlossomMediaType { + &self.media_type + } + + fn from_verified( + descriptor: &RadrootsBlossomByteVerifiedDescriptor, + url: RadrootsBlossomApprovedBlobUrl, + ) -> Self { + Self { + url, + sha256: descriptor.sha256(), + size: descriptor.size(), + media_type: descriptor.media_type().clone(), + } + } + + fn from_wire(wire: MediaReferenceWire) -> Result<Self, RadrootsPhase1PublicationArtifactError> { + let parsed_url = RadrootsBlossomBlobUrl::parse(&wire.url) + .and_then(RadrootsBlossomBlobUrl::approve) + .map_err(|_| RadrootsPhase1PublicationArtifactError::InvalidMediaReference)?; + if parsed_url.as_str() != wire.url { + return Err(RadrootsPhase1PublicationArtifactError::InvalidMediaReference); + } + let sha256 = RadrootsBlossomSha256::from_hex(&wire.sha256) + .map_err(|_| RadrootsPhase1PublicationArtifactError::InvalidMediaReference)?; + if parsed_url.as_blob_url().hash_path().hash() != sha256 { + return Err(RadrootsPhase1PublicationArtifactError::InvalidMediaReference); + } + let media_type = RadrootsBlossomMediaType::parse(&wire.media_type) + .map_err(|_| RadrootsPhase1PublicationArtifactError::InvalidMediaReference)?; + if media_type.as_str() != wire.media_type || !wire.media_type.starts_with("image/") { + return Err(RadrootsPhase1PublicationArtifactError::InvalidMediaReference); + } + Ok(Self { + url: parsed_url, + sha256, + size: wire.size, + media_type, + }) + } + + fn to_wire(&self) -> MediaReferenceWire { + MediaReferenceWire { + url: self.url.as_str().to_string(), + sha256: self.sha256.to_hex(), + size: self.size, + media_type: self.media_type.as_str().to_string(), + } + } +} + +/// Domain-separated SHA-256 identity for the canonical artifact payload. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RadrootsPhase1PublicationArtifactDigest([u8; 32]); + +impl RadrootsPhase1PublicationArtifactDigest { + pub const fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } + + pub fn to_hex(self) -> String { + hex::encode(self.0) + } + + fn parse(value: &str) -> Result<Self, RadrootsPhase1PublicationArtifactError> { + if value.len() != 64 + || !value + .bytes() + .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()) + { + return Err(RadrootsPhase1PublicationArtifactError::InvalidDigest); + } + let bytes = hex::decode(value) + .map_err(|_| RadrootsPhase1PublicationArtifactError::InvalidDigest)?; + let value: [u8; 32] = bytes + .try_into() + .map_err(|_| RadrootsPhase1PublicationArtifactError::InvalidDigest)?; + Ok(Self(value)) + } +} + +impl fmt::Display for RadrootsPhase1PublicationArtifactDigest { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&self.to_hex()) + } +} + +/// A versioned artifact whose exact canonical bytes are ready for persistence. +/// +/// There is deliberately no constructor from `RadrootsEventDraft`, arbitrary +/// wire parts, raw JSON, numeric kind, or a signed event. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RadrootsPhase1PublicationArtifact { + semantic_variant: RadrootsPhase1PublicationSemanticVariant, + expected_author: RadrootsPublicKey, + draft: RadrootsPhase1PublicationDraft, + expected_event_id: RadrootsEventId, + media_references: Vec<RadrootsPhase1PublicationMediaReference>, + artifact_digest: RadrootsPhase1PublicationArtifactDigest, + canonical_json: Vec<u8>, +} + +impl RadrootsPhase1PublicationArtifact { + pub fn from_profile( + profile: &RadrootsAuthoredProfile, + created_at: u64, + expected_author: impl AsRef<str>, + ) -> Result<Self, RadrootsPhase1PublicationArtifactError> { + let parts = profile_authored::authored_profile_to_wire_parts(profile) + .map_err(|error| encoding_error(error.code()))?; + let mut media = Vec::with_capacity(2); + for image in [profile.picture(), profile.banner()].into_iter().flatten() { + let descriptor = image.descriptor(); + media.push(RadrootsPhase1PublicationMediaReference::from_verified( + descriptor, + descriptor.url().clone(), + )); + } + Self::from_authored_parts( + RadrootsPhase1PublicationSemanticVariant::Profile, + created_at, + expected_author.as_ref(), + parts, + media, + ) + } + + pub fn from_update( + update: &RadrootsAuthoredUpdate, + created_at: u64, + expected_author: impl AsRef<str>, + ) -> Result<Self, RadrootsPhase1PublicationArtifactError> { + Self::from_authored_parts( + RadrootsPhase1PublicationSemanticVariant::Update, + created_at, + expected_author.as_ref(), + post_authored::authored_update_to_wire_parts(update), + Vec::new(), + ) + } + + pub fn from_photo_update( + photo_update: &RadrootsAuthoredPhotoUpdate, + created_at: u64, + expected_author: impl AsRef<str>, + ) -> Result<Self, RadrootsPhase1PublicationArtifactError> { + Self::from_authored_parts( + RadrootsPhase1PublicationSemanticVariant::PhotoUpdate, + created_at, + expected_author.as_ref(), + post_authored::authored_photo_update_to_wire_parts(photo_update), + post_media_references(photo_update.images()), + ) + } + + pub fn from_ask( + ask: &RadrootsAuthoredAsk, + created_at: u64, + expected_author: impl AsRef<str>, + ) -> Result<Self, RadrootsPhase1PublicationArtifactError> { + Self::from_authored_parts( + RadrootsPhase1PublicationSemanticVariant::Ask, + created_at, + expected_author.as_ref(), + post_authored::authored_ask_to_wire_parts(ask), + post_media_references(ask.images()), + ) + } + + pub fn from_calendar_date_event( + event: &RadrootsAuthoredCalendarDateEvent, + created_at: u64, + expected_author: impl AsRef<str>, + ) -> Result<Self, RadrootsPhase1PublicationArtifactError> { + let parts = + encode::date_to_wire_parts(event).map_err(|error| encoding_error(error.code()))?; + Self::from_authored_parts( + RadrootsPhase1PublicationSemanticVariant::Event( + RadrootsPhase1PublicationEventVariant::Date, + ), + created_at, + expected_author.as_ref(), + parts, + authored_image_reference(event.image()), + ) + } + + pub fn from_calendar_time_event( + event: &RadrootsAuthoredCalendarTimeEvent, + created_at: u64, + expected_author: impl AsRef<str>, + ) -> Result<Self, RadrootsPhase1PublicationArtifactError> { + let parts = + encode::time_to_wire_parts(event).map_err(|error| encoding_error(error.code()))?; + Self::from_authored_parts( + RadrootsPhase1PublicationSemanticVariant::Event( + RadrootsPhase1PublicationEventVariant::Time, + ), + created_at, + expected_author.as_ref(), + parts, + authored_image_reference(event.image()), + ) + } + + pub fn from_food_availability( + details: &RadrootsFoodAvailabilityDetails, + created_at: u64, + expected_author: impl AsRef<str>, + ) -> Result<Self, RadrootsPhase1PublicationArtifactError> { + let parts = food_authored::authored_food_availability_to_wire_parts(details, created_at) + .map_err(|error| encoding_error(error.code()))?; + let media = details + .images() + .iter() + .map(|image| { + let descriptor = image.image().descriptor(); + RadrootsPhase1PublicationMediaReference::from_verified( + descriptor, + descriptor.url().clone(), + ) + }) + .collect(); + Self::from_authored_parts( + RadrootsPhase1PublicationSemanticVariant::FoodAvailability, + created_at, + expected_author.as_ref(), + parts, + media, + ) + } + + pub const fn schema_version(&self) -> u32 { + RADROOTS_PHASE1_PUBLICATION_ARTIFACT_SCHEMA_VERSION + } + + pub const fn semantic_variant(&self) -> RadrootsPhase1PublicationSemanticVariant { + self.semantic_variant + } + + 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() + } + + pub fn expected_author(&self) -> &RadrootsPublicKey { + &self.expected_author + } + + pub fn draft(&self) -> &RadrootsPhase1PublicationDraft { + &self.draft + } + + pub fn expected_event_id(&self) -> &RadrootsEventId { + &self.expected_event_id + } + + pub fn media_references(&self) -> &[RadrootsPhase1PublicationMediaReference] { + &self.media_references + } + + pub const fn artifact_digest(&self) -> RadrootsPhase1PublicationArtifactDigest { + self.artifact_digest + } + + pub fn to_canonical_json(&self) -> Vec<u8> { + self.canonical_json.clone() + } + + pub fn from_canonical_json( + bytes: &[u8], + ) -> Result<Self, RadrootsPhase1PublicationArtifactError> { + if bytes.len() > RADROOTS_PHASE1_PUBLICATION_ARTIFACT_MAX_BYTES { + return Err(RadrootsPhase1PublicationArtifactError::ArtifactTooLarge { + max: RADROOTS_PHASE1_PUBLICATION_ARTIFACT_MAX_BYTES, + actual: bytes.len(), + }); + } + let wire: ArtifactWire = serde_json::from_slice(bytes) + .map_err(|_| RadrootsPhase1PublicationArtifactError::InvalidJson)?; + if wire.schema_version != RADROOTS_PHASE1_PUBLICATION_ARTIFACT_SCHEMA_VERSION { + return Err( + RadrootsPhase1PublicationArtifactError::UnsupportedSchemaVersion { + expected: RADROOTS_PHASE1_PUBLICATION_ARTIFACT_SCHEMA_VERSION, + actual: wire.schema_version, + }, + ); + } + let variant = RadrootsPhase1PublicationSemanticVariant::parse(&wire.semantic_variant)?; + let profile = profile_for_variant(variant); + if wire.authored_operation_id != profile.operation_id { + return Err(RadrootsPhase1PublicationArtifactError::AuthoredOperationMismatch); + } + if wire.event_contract_id != profile.contract_id { + return Err(RadrootsPhase1PublicationArtifactError::EventContractMismatch); + } + if wire.draft.kind != profile.kind { + return Err(RadrootsPhase1PublicationArtifactError::KindMismatch { + expected: profile.kind, + actual: wire.draft.kind, + }); + } + let expected_author = parse_expected_author(&wire.expected_author)?; + let expected_event_id = parse_expected_event_id(&wire.expected_event_id)?; + let draft = RadrootsPhase1PublicationDraft { + created_at: wire.draft.created_at, + kind: wire.draft.kind, + tags: wire.draft.tags, + content: wire.draft.content, + }; + validate_draft_identifier(&expected_author, &draft, &expected_event_id)?; + validate_signed_event_wire_size(&expected_author, &draft, &expected_event_id)?; + + if wire.media_references.len() > RADROOTS_PHASE1_PUBLICATION_MEDIA_MAX_COUNT { + return Err( + RadrootsPhase1PublicationArtifactError::TooManyMediaReferences { + max: RADROOTS_PHASE1_PUBLICATION_MEDIA_MAX_COUNT, + actual: wire.media_references.len(), + }, + ); + } + let original_media_wire = wire.media_references; + let mut media_references = original_media_wire + .iter() + .cloned() + .map(RadrootsPhase1PublicationMediaReference::from_wire) + .collect::<Result<Vec<_>, _>>()?; + canonicalize_media_references(&mut media_references)?; + if media_references + .iter() + .map(RadrootsPhase1PublicationMediaReference::to_wire) + .ne(original_media_wire.iter().cloned()) + { + return Err(RadrootsPhase1PublicationArtifactError::NonCanonicalMediaInventory); + } + validate_phase1_publication_profile(variant, &draft, &media_references)?; + + let artifact_digest = + RadrootsPhase1PublicationArtifactDigest::parse(&wire.artifact_digest)?; + let computed = compute_artifact_digest( + variant, + &expected_author, + &draft, + &expected_event_id, + &media_references, + )?; + if computed != artifact_digest { + return Err(RadrootsPhase1PublicationArtifactError::DigestMismatch); + } + let canonical_json = serialize_artifact( + variant, + &expected_author, + &draft, + &expected_event_id, + &media_references, + artifact_digest, + )?; + if canonical_json != bytes { + return Err(RadrootsPhase1PublicationArtifactError::NonCanonicalJson); + } + Ok(Self { + semantic_variant: variant, + expected_author, + draft, + expected_event_id, + media_references, + artifact_digest, + canonical_json, + }) + } + + fn from_authored_parts( + variant: RadrootsPhase1PublicationSemanticVariant, + created_at: u64, + expected_author: &str, + parts: RadrootsNip01EventWireParts, + mut media_references: Vec<RadrootsPhase1PublicationMediaReference>, + ) -> Result<Self, RadrootsPhase1PublicationArtifactError> { + let expected_author = parse_expected_author(expected_author)?; + let profile = profile_for_variant(variant); + if parts.kind != profile.kind { + return Err(RadrootsPhase1PublicationArtifactError::KindMismatch { + expected: profile.kind, + actual: parts.kind, + }); + } + if media_references.len() > RADROOTS_PHASE1_PUBLICATION_MEDIA_MAX_COUNT { + return Err( + RadrootsPhase1PublicationArtifactError::TooManyMediaReferences { + max: RADROOTS_PHASE1_PUBLICATION_MEDIA_MAX_COUNT, + actual: media_references.len(), + }, + ); + } + canonicalize_media_references(&mut media_references)?; + let expected_event_id = compute_canonical_nip01_event_id( + expected_author.as_str(), + created_at, + parts.kind, + &parts.tags, + &parts.content, + ) + .map_err(|_| RadrootsPhase1PublicationArtifactError::InvalidExpectedAuthor)?; + let draft = RadrootsPhase1PublicationDraft { + created_at, + kind: parts.kind, + tags: parts.tags, + content: parts.content, + }; + validate_signed_event_wire_size(&expected_author, &draft, &expected_event_id)?; + validate_phase1_publication_profile(variant, &draft, &media_references)?; + let artifact_digest = compute_artifact_digest( + variant, + &expected_author, + &draft, + &expected_event_id, + &media_references, + )?; + let canonical_json = serialize_artifact( + variant, + &expected_author, + &draft, + &expected_event_id, + &media_references, + artifact_digest, + )?; + if canonical_json.len() > RADROOTS_PHASE1_PUBLICATION_ARTIFACT_MAX_BYTES { + return Err(RadrootsPhase1PublicationArtifactError::ArtifactTooLarge { + max: RADROOTS_PHASE1_PUBLICATION_ARTIFACT_MAX_BYTES, + actual: canonical_json.len(), + }); + } + Ok(Self { + semantic_variant: variant, + expected_author, + draft, + expected_event_id, + media_references, + artifact_digest, + canonical_json, + }) + } +} + +/// Revalidates a sealed artifact through the same strict persisted-byte path +/// used by later allowlist and outbox consumers. +pub fn validate_phase1_publication_artifact( + artifact: &RadrootsPhase1PublicationArtifact, +) -> Result<(), RadrootsPhase1PublicationArtifactError> { + let canonical_json = artifact.to_canonical_json(); + let reloaded = RadrootsPhase1PublicationArtifact::from_canonical_json(&canonical_json)?; + if &reloaded != artifact { + return Err(RadrootsPhase1PublicationArtifactError::ArtifactStateMismatch); + } + Ok(()) +} + +#[non_exhaustive] +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RadrootsPhase1PublicationArtifactError { + ArtifactTooLarge { max: usize, actual: usize }, + EventWireTooLarge { max: usize, actual: usize }, + TooManyMediaReferences { max: usize, actual: usize }, + InvalidJson, + NonCanonicalJson, + UnsupportedSchemaVersion { expected: u32, actual: u32 }, + UnknownSemanticVariant, + AuthoredOperationMismatch, + EventContractMismatch, + KindMismatch { expected: u32, actual: u32 }, + InvalidExpectedAuthor, + InvalidExpectedEventId, + ExpectedEventIdMismatch, + InvalidDraft, + InvalidProfile, + InvalidPostProfile, + InvalidCalendarProfile, + InvalidFoodAvailabilityProfile, + InvalidMediaReference, + ConflictingMediaReference, + NonCanonicalMediaInventory, + MediaInventoryMismatch, + InvalidDigest, + DigestMismatch, + ArtifactStateMismatch, + AuthoredEncoding { code: &'static str }, + Serialization, +} + +impl RadrootsPhase1PublicationArtifactError { + pub const fn code(&self) -> &'static str { + match self { + Self::ArtifactTooLarge { .. } => "publication_artifact_too_large", + Self::EventWireTooLarge { .. } => "publication_event_wire_too_large", + Self::TooManyMediaReferences { .. } => "publication_media_count_exceeded", + Self::InvalidJson => "publication_artifact_invalid_json", + Self::NonCanonicalJson => "publication_artifact_non_canonical_json", + Self::UnsupportedSchemaVersion { .. } => "publication_artifact_version_unsupported", + Self::UnknownSemanticVariant => "publication_semantic_variant_unknown", + Self::AuthoredOperationMismatch => "publication_authored_operation_mismatch", + Self::EventContractMismatch => "publication_event_contract_mismatch", + Self::KindMismatch { .. } => "publication_kind_mismatch", + Self::InvalidExpectedAuthor => "publication_expected_author_invalid", + Self::InvalidExpectedEventId => "publication_expected_event_id_invalid", + Self::ExpectedEventIdMismatch => "publication_expected_event_id_mismatch", + Self::InvalidDraft => "publication_draft_invalid", + Self::InvalidProfile => "publication_profile_invalid", + Self::InvalidPostProfile => "publication_post_profile_invalid", + Self::InvalidCalendarProfile => "publication_calendar_profile_invalid", + Self::InvalidFoodAvailabilityProfile => "publication_food_profile_invalid", + Self::InvalidMediaReference => "publication_media_reference_invalid", + Self::ConflictingMediaReference => "publication_media_reference_conflict", + Self::NonCanonicalMediaInventory => "publication_media_inventory_non_canonical", + Self::MediaInventoryMismatch => "publication_media_inventory_mismatch", + Self::InvalidDigest => "publication_artifact_digest_invalid", + Self::DigestMismatch => "publication_artifact_digest_mismatch", + Self::ArtifactStateMismatch => "publication_artifact_state_mismatch", + Self::AuthoredEncoding { code } => code, + Self::Serialization => "publication_artifact_serialization", + } + } +} + +impl fmt::Display for RadrootsPhase1PublicationArtifactError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ArtifactTooLarge { max, actual } => write!( + formatter, + "publication artifact is {actual} bytes; maximum is {max}" + ), + Self::EventWireTooLarge { max, actual } => write!( + formatter, + "publication event wire is {actual} bytes; maximum is {max}" + ), + Self::TooManyMediaReferences { max, actual } => write!( + formatter, + "publication artifact has {actual} media references; maximum is {max}" + ), + Self::UnsupportedSchemaVersion { expected, actual } => write!( + formatter, + "publication artifact schema version must be {expected}, got {actual}" + ), + Self::KindMismatch { expected, actual } => write!( + formatter, + "publication artifact kind must be {expected}, got {actual}" + ), + Self::AuthoredEncoding { code } => { + write!( + formatter, + "strict authored encoding failed with code {code}" + ) + } + error => formatter.write_str(error.code()), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for RadrootsPhase1PublicationArtifactError {} + +fn encoding_error(code: &'static str) -> RadrootsPhase1PublicationArtifactError { + RadrootsPhase1PublicationArtifactError::AuthoredEncoding { code } +} + +fn parse_expected_author( + value: &str, +) -> Result<RadrootsPublicKey, RadrootsPhase1PublicationArtifactError> { + let author = RadrootsPublicKey::parse(value) + .map_err(|_| RadrootsPhase1PublicationArtifactError::InvalidExpectedAuthor)?; + if author.as_str() != value { + return Err(RadrootsPhase1PublicationArtifactError::InvalidExpectedAuthor); + } + Ok(author) +} + +fn parse_expected_event_id( + value: &str, +) -> Result<RadrootsEventId, RadrootsPhase1PublicationArtifactError> { + let event_id = RadrootsEventId::parse(value) + .map_err(|_| RadrootsPhase1PublicationArtifactError::InvalidExpectedEventId)?; + if event_id.as_str() != value { + return Err(RadrootsPhase1PublicationArtifactError::InvalidExpectedEventId); + } + Ok(event_id) +} + +fn validate_draft_identifier( + author: &RadrootsPublicKey, + draft: &RadrootsPhase1PublicationDraft, + expected_event_id: &RadrootsEventId, +) -> Result<(), RadrootsPhase1PublicationArtifactError> { + let computed = compute_canonical_nip01_event_id( + author.as_str(), + draft.created_at, + draft.kind, + &draft.tags, + &draft.content, + ) + .map_err(|_| RadrootsPhase1PublicationArtifactError::InvalidDraft)?; + if &computed != expected_event_id { + return Err(RadrootsPhase1PublicationArtifactError::ExpectedEventIdMismatch); + } + Ok(()) +} + +#[derive(Serialize)] +struct SignedEventSizeWire<'a> { + id: &'a str, + pubkey: &'a str, + created_at: u64, + kind: u32, + tags: &'a [Vec<String>], + content: &'a str, + sig: &'a str, +} + +fn validate_signed_event_wire_size( + author: &RadrootsPublicKey, + draft: &RadrootsPhase1PublicationDraft, + expected_event_id: &RadrootsEventId, +) -> Result<(), RadrootsPhase1PublicationArtifactError> { + let actual = signed_event_wire_size(author, draft, expected_event_id)?; + if actual > RADROOTS_PHASE1_PUBLICATION_SIGNED_EVENT_MAX_BYTES { + return Err(RadrootsPhase1PublicationArtifactError::EventWireTooLarge { + max: RADROOTS_PHASE1_PUBLICATION_SIGNED_EVENT_MAX_BYTES, + actual, + }); + } + Ok(()) +} + +fn signed_event_wire_size( + author: &RadrootsPublicKey, + draft: &RadrootsPhase1PublicationDraft, + expected_event_id: &RadrootsEventId, +) -> Result<usize, RadrootsPhase1PublicationArtifactError> { + let signature = "0".repeat(128); + Ok(serde_json::to_vec(&SignedEventSizeWire { + id: expected_event_id.as_str(), + pubkey: author.as_str(), + created_at: draft.created_at, + kind: draft.kind, + tags: &draft.tags, + content: &draft.content, + sig: &signature, + }) + .map_err(|_| RadrootsPhase1PublicationArtifactError::Serialization)? + .len()) +} + +fn post_media_references( + images: &[radroots_event::post::RadrootsAuthoredPostImage], +) -> Vec<RadrootsPhase1PublicationMediaReference> { + let mut media = Vec::new(); + for image in images { + let descriptor = image.image().descriptor(); + media.push(RadrootsPhase1PublicationMediaReference::from_verified( + descriptor, + descriptor.url().clone(), + )); + media.extend(image.fallbacks().iter().cloned().map(|fallback| { + RadrootsPhase1PublicationMediaReference::from_verified(descriptor, fallback) + })); + } + media +} + +fn authored_image_reference( + image: Option<&radroots_event::RadrootsAuthoredImage>, +) -> Vec<RadrootsPhase1PublicationMediaReference> { + image + .map(|image| { + let descriptor = image.descriptor(); + vec![RadrootsPhase1PublicationMediaReference::from_verified( + descriptor, + descriptor.url().clone(), + )] + }) + .unwrap_or_default() +} + +fn canonicalize_media_references( + media: &mut Vec<RadrootsPhase1PublicationMediaReference>, +) -> Result<(), RadrootsPhase1PublicationArtifactError> { + media.sort_by(|left, right| left.url.as_str().cmp(right.url.as_str())); + let mut index = 1usize; + while index < media.len() { + if media[index - 1].url.as_str() == media[index].url.as_str() { + if media[index - 1] != media[index] { + return Err(RadrootsPhase1PublicationArtifactError::ConflictingMediaReference); + } + media.remove(index); + } else { + index += 1; + } + } + Ok(()) +} + +fn validate_phase1_publication_profile( + variant: RadrootsPhase1PublicationSemanticVariant, + draft: &RadrootsPhase1PublicationDraft, + media: &[RadrootsPhase1PublicationMediaReference], +) -> Result<(), RadrootsPhase1PublicationArtifactError> { + if draft.content.len() > DEFAULT_CONTENT_MAX_BYTES + || RadrootsEventTags::new(draft.tags.clone()).is_err() + { + return Err(RadrootsPhase1PublicationArtifactError::InvalidDraft); + } + match variant { + RadrootsPhase1PublicationSemanticVariant::Profile => validate_profile(draft, media), + RadrootsPhase1PublicationSemanticVariant::Update => validate_update(draft, media), + RadrootsPhase1PublicationSemanticVariant::PhotoUpdate => validate_post(draft, media, false), + RadrootsPhase1PublicationSemanticVariant::Ask => validate_post(draft, media, true), + RadrootsPhase1PublicationSemanticVariant::Event( + RadrootsPhase1PublicationEventVariant::Date, + ) => validate_calendar_date(draft, media), + RadrootsPhase1PublicationSemanticVariant::Event( + RadrootsPhase1PublicationEventVariant::Time, + ) => validate_calendar_time(draft, media), + RadrootsPhase1PublicationSemanticVariant::FoodAvailability => { + validate_food_availability(draft, media) + } + } +} + +#[derive(Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +struct StrictProfileContent { + name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + display_name: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + about: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + picture: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + banner: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + nip05: Option<String>, + #[serde(default, skip_serializing_if = "Option::is_none")] + bot: Option<bool>, +} + +fn validate_profile( + draft: &RadrootsPhase1PublicationDraft, + media: &[RadrootsPhase1PublicationMediaReference], +) -> Result<(), RadrootsPhase1PublicationArtifactError> { + if draft.kind != KIND_PROFILE + || !draft.tags.is_empty() + || draft.content.len() > RADROOTS_PROFILE_METADATA_MAX_CONTENT_BYTES + { + return Err(RadrootsPhase1PublicationArtifactError::InvalidProfile); + } + let content: StrictProfileContent = serde_json::from_str(&draft.content) + .map_err(|_| RadrootsPhase1PublicationArtifactError::InvalidProfile)?; + RadrootsAuthoredProfile::new(&content.name) + .map_err(|_| RadrootsPhase1PublicationArtifactError::InvalidProfile)?; + if let Some(nip05) = content.nip05.as_deref() { + let parsed = RadrootsNip05Identifier::parse(nip05) + .map_err(|_| RadrootsPhase1PublicationArtifactError::InvalidProfile)?; + if parsed.as_str() != nip05 { + return Err(RadrootsPhase1PublicationArtifactError::InvalidProfile); + } + } + let canonical = serde_json::to_string(&content) + .map_err(|_| RadrootsPhase1PublicationArtifactError::Serialization)?; + if canonical != draft.content { + return Err(RadrootsPhase1PublicationArtifactError::InvalidProfile); + } + let urls = content + .picture + .iter() + .chain(content.banner.iter()) + .map(String::as_str) + .collect::<Vec<_>>(); + for url in &urls { + validate_primary_media_url(url, media)?; + } + validate_media_urls(urls.into_iter(), media) +} + +fn validate_update( + draft: &RadrootsPhase1PublicationDraft, + media: &[RadrootsPhase1PublicationMediaReference], +) -> Result<(), RadrootsPhase1PublicationArtifactError> { + let update = RadrootsAuthoredUpdate::new(&draft.content) + .map_err(|_| RadrootsPhase1PublicationArtifactError::InvalidPostProfile)?; + if post_authored::authored_update_to_wire_parts(&update) != draft_wire_parts(draft) + || !media.is_empty() + { + return Err(RadrootsPhase1PublicationArtifactError::InvalidPostProfile); + } + Ok(()) +} + +fn validate_post( + draft: &RadrootsPhase1PublicationDraft, + media: &[RadrootsPhase1PublicationMediaReference], + ask: bool, +) -> Result<(), RadrootsPhase1PublicationArtifactError> { + if ask && RadrootsAuthoredAsk::new(draft.content.as_str(), Vec::new()).is_err() { + return Err(RadrootsPhase1PublicationArtifactError::InvalidPostProfile); + } + let projection = post_inbound::registry_v7::project_inbound_post_parts( + draft.kind, + &draft.tags, + &draft.content, + ) + .map_err(|_| RadrootsPhase1PublicationArtifactError::InvalidPostProfile)?; + let expected_classification = if ask { + post_inbound::RadrootsPostClassification::Ask + } else { + post_inbound::RadrootsPostClassification::PhotoUpdate + }; + if projection.classification() != expected_classification + || !projection.diagnostics().is_empty() + || (!ask && projection.imeta().is_empty()) + { + return Err(RadrootsPhase1PublicationArtifactError::InvalidPostProfile); + } + let mut canonical_tags = Vec::with_capacity(projection.imeta().len() + usize::from(ask)); + if ask { + canonical_tags.push(vec!["t".to_string(), "radroots-ask".to_string()]); + } + let mut urls = Vec::new(); + for imeta in projection.imeta() { + let (Some(url), Some(sha256), Some(media_type), Some(dimensions), Some(size), Some(alt)) = ( + imeta.url(), + imeta.sha256(), + imeta.media_type(), + imeta.dimensions(), + imeta.size(), + imeta.alt(), + ) else { + return Err(RadrootsPhase1PublicationArtifactError::InvalidPostProfile); + }; + let mut tag = vec![ + "imeta".to_string(), + format!("url {url}"), + format!("x {sha256}"), + format!("m {media_type}"), + format!("dim {}x{}", dimensions.width(), dimensions.height()), + format!("size {size}"), + format!("alt {alt}"), + ]; + tag.extend( + imeta + .fallbacks() + .iter() + .map(|fallback| format!("fallback {fallback}")), + ); + canonical_tags.push(tag); + urls.push(url); + urls.extend(imeta.fallbacks().iter().map(String::as_str)); + let reference = media_reference_for_url(media, url)?; + validate_primary_media_reference(reference)?; + if reference.sha256.to_hex() != sha256 + || reference.media_type.as_str() != media_type + || reference.size != size + { + return Err(RadrootsPhase1PublicationArtifactError::MediaInventoryMismatch); + } + for fallback in imeta.fallbacks() { + let fallback_reference = media_reference_for_url(media, fallback)?; + if fallback_reference.sha256 != reference.sha256 + || fallback_reference.media_type != reference.media_type + || fallback_reference.size != reference.size + { + return Err(RadrootsPhase1PublicationArtifactError::MediaInventoryMismatch); + } + } + } + if canonical_tags != draft.tags { + return Err(RadrootsPhase1PublicationArtifactError::InvalidPostProfile); + } + validate_media_urls(urls.into_iter(), media) +} + +fn validate_calendar_date( + draft: &RadrootsPhase1PublicationDraft, + media: &[RadrootsPhase1PublicationMediaReference], +) -> Result<(), RadrootsPhase1PublicationArtifactError> { + let parsed = decode::parse_nip52_calendar_date_event(draft.kind, &draft.tags, &draft.content) + .map_err(|_| RadrootsPhase1PublicationArtifactError::InvalidCalendarProfile)?; + let admitted = decode::admit_radroots_calendar_date_event(parsed) + .map_err(|_| RadrootsPhase1PublicationArtifactError::InvalidCalendarProfile)?; + let canonical = canonical_date_tags(&admitted); + if canonical != draft.tags { + return Err(RadrootsPhase1PublicationArtifactError::InvalidCalendarProfile); + } + validate_calendar_media(admitted.parsed().common(), media) +} + +fn validate_calendar_time( + draft: &RadrootsPhase1PublicationDraft, + media: &[RadrootsPhase1PublicationMediaReference], +) -> Result<(), RadrootsPhase1PublicationArtifactError> { + let parsed = decode::parse_nip52_calendar_time_event(draft.kind, &draft.tags, &draft.content) + .map_err(|_| RadrootsPhase1PublicationArtifactError::InvalidCalendarProfile)?; + let admitted = decode::admit_radroots_calendar_time_event(parsed) + .map_err(|_| RadrootsPhase1PublicationArtifactError::InvalidCalendarProfile)?; + let canonical = canonical_time_tags(&admitted); + if canonical != draft.tags { + return Err(RadrootsPhase1PublicationArtifactError::InvalidCalendarProfile); + } + validate_calendar_media(admitted.parsed().common(), media) +} + +fn canonical_date_tags(event: &RadrootsAdmittedCalendarDateEvent) -> Vec<Vec<String>> { + let parsed = event.parsed(); + let mut tags = vec![ + pair("d", parsed.common().d_tag()), + pair("title", parsed.common().title()), + pair("start", parsed.start().as_str()), + ]; + if let Some(end) = parsed.end() { + tags.push(pair("end", end.as_str())); + } + push_canonical_calendar_common_tags(&mut tags, parsed.common()); + tags +} + +fn canonical_time_tags(event: &RadrootsAdmittedCalendarTimeEvent) -> Vec<Vec<String>> { + let parsed = event.parsed(); + let mut tags = vec![ + pair("d", parsed.common().d_tag()), + pair("title", parsed.common().title()), + pair("start", parsed.start_wire()), + ]; + if let Some(end) = parsed.end_wire() { + tags.push(pair("end", end)); + } + tags.extend( + parsed + .observed_day_indices() + .iter() + .map(|day| pair("D", day.wire_value())), + ); + if let Some(tzid) = parsed.start_tzid() { + tags.push(pair("start_tzid", tzid.as_str())); + } + if let Some(tzid) = parsed.end_tzid() { + tags.push(pair("end_tzid", tzid.as_str())); + } + push_canonical_calendar_common_tags(&mut tags, parsed.common()); + tags +} + +fn push_canonical_calendar_common_tags( + tags: &mut Vec<Vec<String>>, + common: &RadrootsParsedNip52CalendarCommon, +) { + tags.extend( + common + .locations() + .iter() + .map(|value| pair("location", value)), + ); + if let Some(value) = common.geohash() { + tags.push(pair("g", value)); + } + if let Some(value) = common.summary() { + tags.push(pair("summary", value)); + } + if let Some(value) = common.image() { + tags.push(pair("image", value.as_str())); + } + for participant in common.participants() { + let mut tag = vec!["p".to_string(), participant.pubkey.clone()]; + if let Some(relay) = &participant.relay { + tag.push(relay.clone()); + } + if let Some(role) = &participant.role { + if participant.relay.is_none() { + tag.push(String::new()); + } + tag.push(role.clone()); + } + tags.push(tag); + } + tags.extend(common.categories().iter().map(|value| pair("t", value))); + tags.extend( + common + .references() + .iter() + .map(|value| pair("r", value.as_str())), + ); + for request in common.calendar_requests() { + let mut tag = vec!["a".to_string(), request.calendar().as_str().to_string()]; + if let Some(relay) = request.relay() { + tag.push(relay.to_string()); + } + tags.push(tag); + } +} + +fn validate_calendar_media( + common: &RadrootsParsedNip52CalendarCommon, + media: &[RadrootsPhase1PublicationMediaReference], +) -> Result<(), RadrootsPhase1PublicationArtifactError> { + if common.legacy_name().is_some() { + return Err(RadrootsPhase1PublicationArtifactError::InvalidCalendarProfile); + } + if let Some(image) = common.image() { + validate_primary_media_url(image.as_str(), media)?; + } + validate_media_urls(common.image().into_iter().map(|url| url.as_str()), media) +} + +fn validate_food_availability( + draft: &RadrootsPhase1PublicationDraft, + media: &[RadrootsPhase1PublicationMediaReference], +) -> Result<(), RadrootsPhase1PublicationArtifactError> { + let tags = RadrootsEventTags::new(draft.tags.clone()) + .map_err(|_| RadrootsPhase1PublicationArtifactError::InvalidFoodAvailabilityProfile)?; + let outcome = food_inbound::registry_v7::project_inbound_food_availability_parts( + draft.kind, + draft.created_at, + &tags, + &draft.content, + ) + .map_err(|_| RadrootsPhase1PublicationArtifactError::InvalidFoodAvailabilityProfile)?; + let projection = match outcome { + food_inbound::RadrootsFoodAvailabilityProjectionOutcome::Focused(projection) => projection, + _ => return Err(RadrootsPhase1PublicationArtifactError::InvalidFoodAvailabilityProfile), + }; + if !projection.diagnostics().is_empty() { + return Err(RadrootsPhase1PublicationArtifactError::InvalidFoodAvailabilityProfile); + } + let mut canonical = vec![ + pair("d", projection.identifier().as_str()), + pair("title", projection.title().as_str()), + pair("summary", projection.summary().as_str()), + pair("published_at", &projection.published_at().to_string()), + pair("location", projection.location().as_str()), + vec![ + "price".to_string(), + projection.price().amount().to_string(), + projection.price().currency().as_str().to_string(), + ], + pair("radroots:price_unit", projection.price().unit().as_str()), + ]; + if let Some(quantity) = projection.quantity() { + canonical.push(vec![ + "radroots:quantity".to_string(), + quantity.amount().to_string(), + quantity.unit().as_str().to_string(), + ]); + } + canonical.push(pair("status", projection.status().as_str())); + let mut urls = Vec::new(); + for image in projection.images() { + let (Some(url), Some(dimensions)) = (image.url(), image.dimensions()) else { + return Err(RadrootsPhase1PublicationArtifactError::InvalidFoodAvailabilityProfile); + }; + canonical.push(vec![ + "image".to_string(), + url.to_string(), + dimensions.to_string(), + ]); + validate_primary_media_url(url, media)?; + urls.push(url); + } + if canonical != draft.tags || projection.content().as_str() != draft.content { + return Err(RadrootsPhase1PublicationArtifactError::InvalidFoodAvailabilityProfile); + } + validate_media_urls(urls.into_iter(), media) +} + +fn pair(key: &str, value: &str) -> Vec<String> { + vec![key.to_string(), value.to_string()] +} + +fn draft_wire_parts(draft: &RadrootsPhase1PublicationDraft) -> RadrootsNip01EventWireParts { + RadrootsNip01EventWireParts { + kind: draft.kind, + content: draft.content.clone(), + tags: draft.tags.clone(), + } +} + +fn media_reference_for_url<'a>( + media: &'a [RadrootsPhase1PublicationMediaReference], + url: &str, +) -> Result<&'a RadrootsPhase1PublicationMediaReference, RadrootsPhase1PublicationArtifactError> { + media + .binary_search_by(|candidate| candidate.url.as_str().cmp(url)) + .ok() + .map(|index| &media[index]) + .ok_or(RadrootsPhase1PublicationArtifactError::MediaInventoryMismatch) +} + +fn validate_primary_media_url( + url: &str, + media: &[RadrootsPhase1PublicationMediaReference], +) -> Result<(), RadrootsPhase1PublicationArtifactError> { + validate_primary_media_reference(media_reference_for_url(media, url)?) +} + +fn validate_primary_media_reference( + reference: &RadrootsPhase1PublicationMediaReference, +) -> Result<(), RadrootsPhase1PublicationArtifactError> { + if reference + .url + .as_blob_url() + .hash_path() + .extension() + .is_none() + { + return Err(RadrootsPhase1PublicationArtifactError::InvalidMediaReference); + } + Ok(()) +} + +fn validate_media_urls<'a>( + urls: impl Iterator<Item = &'a str>, + media: &[RadrootsPhase1PublicationMediaReference], +) -> Result<(), RadrootsPhase1PublicationArtifactError> { + let mut expected = urls.map(str::to_string).collect::<Vec<_>>(); + expected.sort(); + expected.dedup(); + if expected.len() != media.len() + || expected + .iter() + .zip(media) + .any(|(url, reference)| url != reference.url.as_str()) + { + return Err(RadrootsPhase1PublicationArtifactError::MediaInventoryMismatch); + } + Ok(()) +} + +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +struct DraftWire { + created_at: u64, + kind: u32, + tags: Vec<Vec<String>>, + content: String, +} + +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +struct MediaReferenceWire { + url: String, + sha256: String, + size: u64, + media_type: String, +} + +#[derive(Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +struct ArtifactWire { + schema_version: u32, + semantic_variant: String, + authored_operation_id: String, + event_contract_id: String, + expected_author: String, + draft: DraftWire, + expected_event_id: String, + media_references: Vec<MediaReferenceWire>, + artifact_digest: String, +} + +#[derive(Serialize)] +struct ArtifactPayloadWire<'a> { + schema_version: u32, + semantic_variant: &'a str, + authored_operation_id: &'a str, + event_contract_id: &'a str, + expected_author: &'a str, + draft: DraftWire, + expected_event_id: &'a str, + media_references: Vec<MediaReferenceWire>, +} + +fn artifact_payload_wire<'a>( + variant: RadrootsPhase1PublicationSemanticVariant, + author: &'a RadrootsPublicKey, + draft: &RadrootsPhase1PublicationDraft, + expected_event_id: &'a RadrootsEventId, + media: &[RadrootsPhase1PublicationMediaReference], +) -> ArtifactPayloadWire<'a> { + ArtifactPayloadWire { + schema_version: RADROOTS_PHASE1_PUBLICATION_ARTIFACT_SCHEMA_VERSION, + semantic_variant: variant.as_str(), + authored_operation_id: variant.authored_operation_id(), + event_contract_id: variant.event_contract_id(), + expected_author: author.as_str(), + draft: DraftWire { + created_at: draft.created_at, + kind: draft.kind, + tags: draft.tags.clone(), + content: draft.content.clone(), + }, + expected_event_id: expected_event_id.as_str(), + media_references: media + .iter() + .map(RadrootsPhase1PublicationMediaReference::to_wire) + .collect(), + } +} + +fn compute_artifact_digest( + variant: RadrootsPhase1PublicationSemanticVariant, + author: &RadrootsPublicKey, + draft: &RadrootsPhase1PublicationDraft, + expected_event_id: &RadrootsEventId, + media: &[RadrootsPhase1PublicationMediaReference], +) -> Result<RadrootsPhase1PublicationArtifactDigest, RadrootsPhase1PublicationArtifactError> { + let payload = serde_json::to_vec(&artifact_payload_wire( + variant, + author, + draft, + expected_event_id, + media, + )) + .map_err(|_| RadrootsPhase1PublicationArtifactError::Serialization)?; + let mut hasher = Sha256::new(); + hasher.update(ARTIFACT_DIGEST_DOMAIN); + hasher.update(ARTIFACT_DIGEST_DOMAIN_TERMINATOR); + hasher.update(payload); + Ok(RadrootsPhase1PublicationArtifactDigest( + hasher.finalize().into(), + )) +} + +fn serialize_artifact( + variant: RadrootsPhase1PublicationSemanticVariant, + author: &RadrootsPublicKey, + draft: &RadrootsPhase1PublicationDraft, + expected_event_id: &RadrootsEventId, + media: &[RadrootsPhase1PublicationMediaReference], + digest: RadrootsPhase1PublicationArtifactDigest, +) -> Result<Vec<u8>, RadrootsPhase1PublicationArtifactError> { + let payload = artifact_payload_wire(variant, author, draft, expected_event_id, media); + serde_json::to_vec(&ArtifactWire { + schema_version: payload.schema_version, + semantic_variant: payload.semantic_variant.to_string(), + authored_operation_id: payload.authored_operation_id.to_string(), + event_contract_id: payload.event_contract_id.to_string(), + expected_author: payload.expected_author.to_string(), + draft: payload.draft, + expected_event_id: payload.expected_event_id.to_string(), + media_references: payload.media_references, + artifact_digest: digest.to_hex(), + }) + .map_err(|_| RadrootsPhase1PublicationArtifactError::Serialization) +} + +#[cfg(test)] +mod tests { + use super::*; + + const AUTHOR: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + + #[test] + fn signed_event_wire_size_accepts_exact_limit_and_rejects_one_over() { + let author = RadrootsPublicKey::parse(AUTHOR).unwrap(); + let mut draft = RadrootsPhase1PublicationDraft { + created_at: 1_784_347_200, + kind: KIND_POST, + tags: Vec::new(), + content: String::new(), + }; + let empty_id = event_id(&author, &draft); + let base = signed_event_wire_size(&author, &draft, &empty_id).unwrap(); + let available = RADROOTS_PHASE1_PUBLICATION_SIGNED_EVENT_MAX_BYTES - base; + draft.content = "\0".repeat(available / 6) + &"x".repeat(available % 6); + assert!(draft.content.len() <= DEFAULT_CONTENT_MAX_BYTES); + + let exact_id = event_id(&author, &draft); + assert_eq!( + signed_event_wire_size(&author, &draft, &exact_id).unwrap(), + RADROOTS_PHASE1_PUBLICATION_SIGNED_EVENT_MAX_BYTES + ); + validate_signed_event_wire_size(&author, &draft, &exact_id).unwrap(); + + draft.content.push('x'); + let oversized_id = event_id(&author, &draft); + assert_eq!( + validate_signed_event_wire_size(&author, &draft, &oversized_id), + Err(RadrootsPhase1PublicationArtifactError::EventWireTooLarge { + max: RADROOTS_PHASE1_PUBLICATION_SIGNED_EVENT_MAX_BYTES, + actual: RADROOTS_PHASE1_PUBLICATION_SIGNED_EVENT_MAX_BYTES + 1, + }) + ); + } + + #[test] + fn publication_artifact_digest_has_exactly_one_nul_domain_terminator() { + let author = RadrootsPublicKey::parse(AUTHOR).unwrap(); + let draft = RadrootsPhase1PublicationDraft { + created_at: 1_784_347_200, + kind: KIND_POST, + tags: Vec::new(), + content: "Carrots harvested today".to_string(), + }; + let expected_event_id = event_id(&author, &draft); + let payload = serde_json::to_vec(&artifact_payload_wire( + RadrootsPhase1PublicationSemanticVariant::Update, + &author, + &draft, + &expected_event_id, + &[], + )) + .unwrap(); + let actual = compute_artifact_digest( + RadrootsPhase1PublicationSemanticVariant::Update, + &author, + &draft, + &expected_event_id, + &[], + ) + .unwrap(); + + let mut exact = Sha256::new(); + exact.update(b"radroots.phase1.publication-artifact.v1"); + exact.update([0]); + exact.update(&payload); + let exact: [u8; 32] = exact.finalize().into(); + assert_eq!(actual.as_bytes(), &exact); + + for prefix in [ + b"radroots.phase1.publication-artifact.v1".as_slice(), + b"radroots.phase1.publication-artifact.v1\0\0".as_slice(), + ] { + let mut alternate = Sha256::new(); + alternate.update(prefix); + alternate.update(&payload); + let alternate: [u8; 32] = alternate.finalize().into(); + assert_ne!(actual.as_bytes(), &alternate); + } + } + + fn event_id( + author: &RadrootsPublicKey, + draft: &RadrootsPhase1PublicationDraft, + ) -> RadrootsEventId { + compute_canonical_nip01_event_id( + author.as_str(), + draft.created_at, + draft.kind, + &draft.tags, + &draft.content, + ) + .unwrap() + } +} diff --git a/crates/event_codec/tests/codec_error_job.rs b/crates/event_codec/tests/codec_error_job.rs @@ -1,6 +1,7 @@ #[path = "../src/test_fixtures.rs"] mod test_fixtures; +#[cfg(feature = "std")] use std::error::Error as _; use radroots_event_codec::error::{EventEncodeError, EventParseError}; @@ -20,11 +21,13 @@ fn parse_error_display_and_source_cover_variants() { let missing = EventParseError::MissingTag("d"); assert_eq!(missing.to_string(), "missing tag: d"); assert_eq!(missing.code(), "missing_tag"); + #[cfg(feature = "std")] assert!(missing.source().is_none()); let invalid = EventParseError::InvalidTag("a"); assert_eq!(invalid.to_string(), "invalid tag structure for 'a'"); assert_eq!(invalid.code(), "invalid_tag"); + #[cfg(feature = "std")] assert!(invalid.source().is_none()); let invalid_kind = EventParseError::InvalidKind { @@ -33,6 +36,7 @@ fn parse_error_display_and_source_cover_variants() { }; assert_eq!(invalid_kind.to_string(), "invalid kind 1 (expected 30340)"); assert_eq!(invalid_kind.code(), "invalid_kind"); + #[cfg(feature = "std")] assert!(invalid_kind.source().is_none()); let parse_int = "x".parse::<u32>().expect_err("parse int error"); @@ -43,11 +47,13 @@ fn parse_error_display_and_source_cover_variants() { .contains("invalid number in 'count'") ); assert_eq!(invalid_number.code(), "invalid_number"); + #[cfg(feature = "std")] assert!(invalid_number.source().is_some()); let invalid_json = EventParseError::InvalidJson("content"); assert_eq!(invalid_json.to_string(), "invalid JSON in 'content'"); assert_eq!(invalid_json.code(), "invalid_json"); + #[cfg(feature = "std")] assert!(invalid_json.source().is_none()); } @@ -148,14 +154,17 @@ fn job_encode_error_display_covers_variants() { fn job_parse_error_display_and_source_covers_variants() { let kind = JobParseError::KindOutOfRange(u32::from(u16::MAX) + 1); assert!(kind.to_string().contains("Nostr event kind")); + #[cfg(feature = "std")] assert!(kind.source().is_none()); let missing = JobParseError::MissingTag("e"); assert_eq!(missing.to_string(), "missing tag: e"); + #[cfg(feature = "std")] assert!(missing.source().is_none()); let invalid = JobParseError::InvalidTag("status"); assert_eq!(invalid.to_string(), "invalid tag structure for 'status'"); + #[cfg(feature = "std")] assert!(invalid.source().is_none()); let invalid_number = JobParseError::InvalidNumber("amount", "x".parse::<u32>().unwrap_err()); @@ -164,17 +173,21 @@ fn job_parse_error_display_and_source_covers_variants() { .to_string() .contains("invalid number in 'amount'") ); + #[cfg(feature = "std")] assert!(invalid_number.source().is_some()); let non_whole = JobParseError::NonWholeSats("amount"); assert!(non_whole.to_string().contains("whole number of sats")); + #[cfg(feature = "std")] assert!(non_whole.source().is_none()); let overflow = JobParseError::AmountOverflow("amount"); assert!(overflow.to_string().contains("does not fit u32 sat")); + #[cfg(feature = "std")] assert!(overflow.source().is_none()); let missing_chain = JobParseError::MissingChainTag("e"); assert_eq!(missing_chain.to_string(), "missing required chain tag: e"); + #[cfg(feature = "std")] assert!(missing_chain.source().is_none()); } diff --git a/crates/event_codec/tests/fixtures/phase1_publication_artifact.v1.json b/crates/event_codec/tests/fixtures/phase1_publication_artifact.v1.json @@ -0,0 +1,335 @@ +{ + "suite": "phase1_publication_artifact", + "contract_version": "1.0.0", + "vectors": [ + { + "id": "profile_round_trip", + "kind": "publication_artifact.build_profile.valid", + "input": { "fixture": "profile" }, + "expected": { + "semantic_variant": "profile", + "operation_id": "profile.build_authored_draft", + "contract_id": "radroots.profile.metadata.v1", + "event_kind": 0, + "media_count": 2, + "expected_event_id": "a7d081b9c142e3a68245bafe1921b0e0dd88f886406e7165a9729f6c57026289", + "artifact_digest": "e296ed63312d58b4e3ee160d4ca3457eb922920d3116329ca498f08e402215b0", + "canonical_json_bytes": 1305, + "canonical_json_sha256": "131d8131bb821179e6fd64c5201d01bee384451d09c20d789860439ef05620e8", + "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\"}" + } + }, + { + "id": "update_round_trip", + "kind": "publication_artifact.build_update.valid", + "input": { "fixture": "update" }, + "expected": { + "semantic_variant": "update", + "operation_id": "social.update.build_authored_draft", + "contract_id": "radroots.social.update.v1", + "event_kind": 1, + "media_count": 0, + "expected_event_id": "7f2d9dbbd6d2f3db1e83338ea2e669b2458e62ded159de15a3fa98dcf9f164e3", + "artifact_digest": "9ad318496bd4a710fbc7f3e0f6d5a01de808d352db91ca56a12e710904784cc5", + "canonical_json_bytes": 525, + "canonical_json_sha256": "21be0d18a7f5812f7fa14600f46e0948480d2525eb1dcab416bb4ef99ca731c6", + "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\"}" + } + }, + { + "id": "photo_update_round_trip", + "kind": "publication_artifact.build_photo_update.valid", + "input": { "fixture": "photo_update" }, + "expected": { + "semantic_variant": "photo_update", + "operation_id": "social.photo_update.build_authored_draft", + "contract_id": "radroots.social.photo_update.v1", + "event_kind": 1, + "media_count": 2, + "expected_event_id": "e592229776cd2a0d0e25a701f4629c0a5dc6e08481a235623d50da6ee8e8f438", + "artifact_digest": "9ed6b7a6fcec3b07bd0c9179fd5b6ecf74517390051e3c2bd1659be62a26c197", + "canonical_json_bytes": 1415, + "canonical_json_sha256": "38caab5242ae70fe2e36b39658dba7e6f723b2bd4fa512c8f166387c17faf69c", + "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\"}" + } + }, + { + "id": "ask_round_trip_with_fallback", + "kind": "publication_artifact.build_ask.valid", + "input": { "fixture": "ask" }, + "expected": { + "semantic_variant": "ask", + "operation_id": "social.ask.build_authored_draft", + "contract_id": "radroots.social.ask.v1", + "event_kind": 1, + "media_count": 2, + "expected_event_id": "bd45fbbb5bfb8a5aa32df81d240e5f3bd56b0f8cde3a20b3f7d455801373fe48", + "artifact_digest": "41eddea199bba97adebef4ce433ebfa05d97978ce8464ed3d52762ab717c2a3c", + "canonical_json_bytes": 1411, + "canonical_json_sha256": "4985ad0919758e3747f0548409854deb77b9734825ab597623e5c6b566e2d5c7", + "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\"}" + } + }, + { + "id": "event_date_round_trip", + "kind": "publication_artifact.build_calendar_date_event.valid", + "input": { "fixture": "event_date" }, + "expected": { + "semantic_variant": "event_date", + "operation_id": "social.calendar_date_event.build_authored_draft", + "contract_id": "radroots.calendar.date_event.v1", + "event_kind": 31922, + "media_count": 1, + "expected_event_id": "bea82662b6acf2f1272c8335a24bb0957e35d22c2b29d3e30c094ad7398dc7c7", + "artifact_digest": "0ec5162ab80988999aac62329d4cfc9c7efce6a82bbda539a723347d73596eb3", + "canonical_json_bytes": 1013, + "canonical_json_sha256": "46b015e34556762f3d7ca592cd3dc6d4d5652ff085c7d8a8dc458d5897cf0b22", + "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\"}" + } + }, + { + "id": "event_time_round_trip", + "kind": "publication_artifact.build_calendar_time_event.valid", + "input": { "fixture": "event_time" }, + "expected": { + "semantic_variant": "event_time", + "operation_id": "social.calendar_time_event.build_authored_draft", + "contract_id": "radroots.calendar.time_event.v1", + "event_kind": 31923, + "media_count": 1, + "expected_event_id": "2088ed11cd8c1495a82e6f5198ed2ee98e0cbc599f22d7f3380892155dc55894", + "artifact_digest": "56bfb4bed87559a58922343905a73c0c96dd0a57f94ff0955caecf0589198638", + "canonical_json_bytes": 1013, + "canonical_json_sha256": "58e3c7e684bc774c35b08b246075076b5286d51639eb0651ed5b6362d958639e", + "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\"}" + } + }, + { + "id": "food_availability_round_trip", + "kind": "publication_artifact.build_food_availability.valid", + "input": { "fixture": "food_availability" }, + "expected": { + "semantic_variant": "food_availability", + "operation_id": "food_availability.build_authored_draft", + "contract_id": "radroots.food.availability.v1", + "event_kind": 30402, + "media_count": 1, + "expected_event_id": "76b60f1b178c0dbdc14a87de36151ea6b04a866ce50e34a0ccae2f406f514e81", + "artifact_digest": "f0989cb993194af4a2f907ad8337e50378d2757c536a09afb4055a681043a7d8", + "canonical_json_bytes": 1132, + "canonical_json_sha256": "525dc96e87b79b1ef3aa67c711cccd8b8e6c205665770ebcb8283035b7b496da", + "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\"}" + } + }, + { + "id": "canonical_json_serialization", + "kind": "publication_artifact.to_canonical_json.valid", + "input": { "fixture": "profile" }, + "expected": { + "canonical_json_bytes": 1305, + "canonical_json_sha256": "131d8131bb821179e6fd64c5201d01bee384451d09c20d789860439ef05620e8" + } + }, + { + "id": "canonical_json_reload", + "kind": "publication_artifact.from_canonical_json.valid", + "input": { "fixture": "profile" }, + "expected": { + "semantic_variant": "profile", + "canonical_json_sha256": "131d8131bb821179e6fd64c5201d01bee384451d09c20d789860439ef05620e8" + } + }, + { + "id": "leading_whitespace_is_noncanonical", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "leading_whitespace" }, + "expected": { "error": "publication_artifact_non_canonical_json" } + }, + { + "id": "artifact_exact_byte_limit_reaches_parser", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "artifact_exact_byte_limit" }, + "expected": { "error": "publication_artifact_invalid_json" } + }, + { + "id": "artifact_one_byte_over_limit_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "artifact_one_over_byte_limit" }, + "expected": { "error": "publication_artifact_too_large" } + }, + { + "id": "unknown_field_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "unknown_field" }, + "expected": { "error": "publication_artifact_invalid_json" } + }, + { + "id": "unknown_draft_field_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "unknown_draft_field" }, + "expected": { "error": "publication_artifact_invalid_json" } + }, + { + "id": "nested_expected_event_id_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "nested_expected_event_id" }, + "expected": { "error": "publication_artifact_invalid_json" } + }, + { + "id": "missing_expected_event_id_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "missing_expected_event_id" }, + "expected": { "error": "publication_artifact_invalid_json" } + }, + { + "id": "malformed_expected_event_id_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "malformed_expected_event_id" }, + "expected": { "error": "publication_expected_event_id_invalid" } + }, + { + "id": "uppercase_expected_event_id_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "uppercase_expected_event_id" }, + "expected": { "error": "publication_expected_event_id_invalid" } + }, + { + "id": "duplicate_expected_event_id_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "duplicate_expected_event_id" }, + "expected": { "error": "publication_artifact_invalid_json" } + }, + { + "id": "unknown_media_field_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "unknown_media_field" }, + "expected": { "error": "publication_artifact_invalid_json" } + }, + { + "id": "json_field_order_is_noncanonical", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "json_field_order" }, + "expected": { "error": "publication_artifact_non_canonical_json" } + }, + { + "id": "unknown_version_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "schema_version" }, + "expected": { "error": "publication_artifact_version_unsupported" } + }, + { + "id": "cross_variant_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "all_cross_variants" }, + "expected": { "error": "publication_authored_operation_mismatch" } + }, + { + "id": "operation_mismatch_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "operation_id" }, + "expected": { "error": "publication_authored_operation_mismatch" } + }, + { + "id": "contract_mismatch_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "contract_id" }, + "expected": { "error": "publication_event_contract_mismatch" } + }, + { + "id": "kind_mismatch_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "kind" }, + "expected": { "error": "publication_kind_mismatch" } + }, + { + "id": "author_mismatch_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "author" }, + "expected": { "error": "publication_expected_author_invalid" } + }, + { + "id": "created_at_tamper_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "created_at" }, + "expected": { "error": "publication_expected_event_id_mismatch" } + }, + { + "id": "draft_tags_tamper_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "draft_tags" }, + "expected": { "error": "publication_expected_event_id_mismatch" } + }, + { + "id": "draft_content_tamper_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "draft_content" }, + "expected": { "error": "publication_expected_event_id_mismatch" } + }, + { + "id": "noncanonical_nip05_is_rejected_after_id_rebuild", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "noncanonical_nip05" }, + "expected": { "error": "publication_profile_invalid" } + }, + { + "id": "empty_ask_content_is_rejected_after_id_rebuild", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "ask", "mutation": "empty_ask_content" }, + "expected": { "error": "publication_post_profile_invalid" } + }, + { + "id": "expected_event_id_tamper_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "expected_event_id" }, + "expected": { "error": "publication_expected_event_id_mismatch" } + }, + { + "id": "digest_tamper_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "digest" }, + "expected": { "error": "publication_artifact_digest_mismatch" } + }, + { + "id": "media_order_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "media_order" }, + "expected": { "error": "publication_media_inventory_non_canonical" } + }, + { + "id": "profile_media_commitment_tamper_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "media_size" }, + "expected": { "error": "publication_artifact_digest_mismatch" } + }, + { + "id": "media_url_tamper_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "media_url" }, + "expected": { "error": "publication_media_inventory_mismatch" } + }, + { + "id": "noncanonical_media_url_casing_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "media_url_casing" }, + "expected": { "error": "publication_media_reference_invalid" } + }, + { + "id": "media_hash_tamper_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "media_hash" }, + "expected": { "error": "publication_media_reference_invalid" } + }, + { + "id": "media_type_tamper_is_rejected", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "profile", "mutation": "media_type" }, + "expected": { "error": "publication_artifact_digest_mismatch" } + }, + { + "id": "post_media_commitment_must_match_imeta", + "kind": "publication_artifact.from_canonical_json.invalid", + "input": { "fixture": "photo_update", "mutation": "media_size" }, + "expected": { "error": "publication_media_inventory_mismatch" } + } + ] +} diff --git a/crates/event_codec/tests/publication_artifact.rs b/crates/event_codec/tests/publication_artifact.rs @@ -0,0 +1,936 @@ +#![cfg(feature = "serde_json")] + +use radroots_blossom::{ + RadrootsBlossomBlobDescriptor, RadrootsBlossomBlobUrl, RadrootsBlossomByteVerifiedDescriptor, + RadrootsBlossomMediaType, RadrootsBlossomSha256, +}; +use radroots_event::{ + RadrootsAuthoredImage, + calendar::{ + RadrootsAuthoredCalendarDateEvent, RadrootsAuthoredCalendarTimeEvent, RadrootsCalendarDate, + }, + food_availability::{ + RadrootsFoodAvailabilityDetails, RadrootsFoodAvailabilityDetailsParts, + RadrootsFoodAvailabilityImage, RadrootsFoodAvailabilityStatus, RadrootsFoodContent, + RadrootsFoodCurrency, RadrootsFoodIdentifier, RadrootsFoodImageDimensions, + RadrootsFoodPrice, RadrootsFoodPublishedAt, RadrootsFoodQuantity, RadrootsFoodText, + RadrootsFoodUnit, + }, + post::{ + RadrootsAuthoredAsk, RadrootsAuthoredPhotoUpdate, RadrootsAuthoredPostImage, + RadrootsAuthoredUpdate, RadrootsPostImageDimensions, + }, + profile::{RadrootsAuthoredProfile, RadrootsNip05Identifier}, + wire::compute_canonical_nip01_event_id, +}; +use radroots_event_codec::wire::publication::{ + RADROOTS_PHASE1_PUBLICATION_ARTIFACT_MAX_BYTES, RADROOTS_PHASE1_PUBLICATION_MEDIA_MAX_COUNT, + RadrootsPhase1PublicationArtifact, RadrootsPhase1PublicationArtifactError, + RadrootsPhase1PublicationEventVariant, RadrootsPhase1PublicationSemanticVariant, + validate_phase1_publication_artifact, +}; +use serde::Deserialize; +use serde_json::Value; + +const AUTHOR: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const CREATED_AT: u64 = 1_784_347_200; + +const PUBLICATION_ARTIFACT_VECTOR: &str = + include_str!("fixtures/phase1_publication_artifact.v1.json"); + +#[derive(Deserialize)] +struct VectorSuite { + suite: String, + contract_version: String, + vectors: Vec<VectorCase>, +} + +#[derive(Deserialize)] +struct VectorCase { + id: String, + kind: String, + input: VectorInput, + expected: VectorExpected, +} + +#[derive(Deserialize)] +struct VectorInput { + fixture: String, + mutation: Option<String>, +} + +#[derive(Deserialize)] +struct VectorExpected { + semantic_variant: Option<String>, + operation_id: Option<String>, + contract_id: Option<String>, + event_kind: Option<u32>, + media_count: Option<usize>, + expected_event_id: Option<String>, + artifact_digest: Option<String>, + canonical_json_bytes: Option<usize>, + canonical_json_sha256: Option<String>, + canonical_json: Option<String>, + error: Option<String>, +} + +#[test] +fn publication_artifact_conformance_vector_executes_every_case() { + let suite: VectorSuite = serde_json::from_str(PUBLICATION_ARTIFACT_VECTOR).unwrap(); + assert_eq!(suite.suite, "phase1_publication_artifact"); + assert_eq!(suite.contract_version, "1.0.0"); + assert_eq!(suite.vectors.len(), 41); + let artifacts = all_artifacts(); + + for case in suite.vectors { + let artifact = artifact_fixture(&artifacts, &case.input.fixture); + match case.kind.as_str() { + kind if kind.starts_with("publication_artifact.build_") && kind.ends_with(".valid") => { + assert_eq!(case.input.mutation, None, "{}", case.id); + assert_eq!( + artifact.semantic_variant().as_str(), + case.expected.semantic_variant.as_deref().unwrap(), + "{}", + case.id + ); + assert_eq!( + artifact.authored_operation_id(), + case.expected.operation_id.as_deref().unwrap(), + "{}", + case.id + ); + assert_eq!( + artifact.event_contract_id(), + case.expected.contract_id.as_deref().unwrap(), + "{}", + case.id + ); + assert_eq!( + artifact.draft().kind(), + case.expected.event_kind.unwrap(), + "{}", + case.id + ); + assert_eq!( + artifact.media_references().len(), + case.expected.media_count.unwrap(), + "{}", + case.id + ); + assert_eq!( + artifact.expected_event_id().as_str(), + case.expected.expected_event_id.as_deref().unwrap(), + "{}", + case.id + ); + assert_eq!( + artifact.artifact_digest().to_string(), + case.expected.artifact_digest.unwrap(), + "{}", + case.id + ); + assert_eq!( + artifact.to_canonical_json().len(), + case.expected.canonical_json_bytes.unwrap(), + "{}", + case.id + ); + assert_eq!( + RadrootsBlossomSha256::digest(&artifact.to_canonical_json()).to_hex(), + case.expected.canonical_json_sha256.unwrap(), + "{}", + case.id + ); + assert_eq!( + artifact.to_canonical_json(), + case.expected.canonical_json.unwrap().as_bytes(), + "{}", + case.id + ); + assert_eq!( + RadrootsPhase1PublicationArtifact::from_canonical_json( + &artifact.to_canonical_json() + ) + .unwrap(), + *artifact, + "{}", + case.id + ); + } + "publication_artifact.to_canonical_json.valid" => { + assert_eq!(case.input.mutation, None, "{}", case.id); + let bytes = artifact.to_canonical_json(); + assert_eq!( + bytes.len(), + case.expected.canonical_json_bytes.unwrap(), + "{}", + case.id + ); + assert_eq!( + RadrootsBlossomSha256::digest(&bytes).to_hex(), + case.expected.canonical_json_sha256.unwrap(), + "{}", + case.id + ); + } + "publication_artifact.from_canonical_json.valid" => { + assert_eq!(case.input.mutation, None, "{}", case.id); + let bytes = artifact.to_canonical_json(); + let reloaded = + RadrootsPhase1PublicationArtifact::from_canonical_json(&bytes).unwrap(); + assert_eq!( + reloaded.semantic_variant().as_str(), + case.expected.semantic_variant.as_deref().unwrap(), + "{}", + case.id + ); + assert_eq!( + RadrootsBlossomSha256::digest(&reloaded.to_canonical_json()).to_hex(), + case.expected.canonical_json_sha256.unwrap(), + "{}", + case.id + ); + assert_eq!(reloaded, *artifact, "{}", case.id); + } + "publication_artifact.from_canonical_json.invalid" => { + if case.input.mutation.as_deref() == Some("all_cross_variants") { + assert_every_cross_variant_is_rejected( + &artifacts, + case.expected.error.as_deref().unwrap(), + &case.id, + ); + continue; + } + let bytes = mutate_artifact( + &artifact.to_canonical_json(), + case.input.mutation.as_deref().unwrap(), + ); + assert_eq!( + RadrootsPhase1PublicationArtifact::from_canonical_json(&bytes) + .unwrap_err() + .code(), + case.expected.error.as_deref().unwrap(), + "{}", + case.id + ); + } + other => panic!("{} has unknown case kind {other}", case.id), + } + } +} + +#[test] +fn publication_artifact_round_trips_every_closed_variant() { + let artifacts = all_artifacts(); + let expected = [ + ( + RadrootsPhase1PublicationSemanticVariant::Profile, + "profile.build_authored_draft", + "radroots.profile.metadata.v1", + 0, + 2, + ), + ( + RadrootsPhase1PublicationSemanticVariant::Update, + "social.update.build_authored_draft", + "radroots.social.update.v1", + 1, + 0, + ), + ( + RadrootsPhase1PublicationSemanticVariant::PhotoUpdate, + "social.photo_update.build_authored_draft", + "radroots.social.photo_update.v1", + 1, + 2, + ), + ( + RadrootsPhase1PublicationSemanticVariant::Ask, + "social.ask.build_authored_draft", + "radroots.social.ask.v1", + 1, + 2, + ), + ( + RadrootsPhase1PublicationSemanticVariant::Event( + RadrootsPhase1PublicationEventVariant::Date, + ), + "social.calendar_date_event.build_authored_draft", + "radroots.calendar.date_event.v1", + 31_922, + 1, + ), + ( + RadrootsPhase1PublicationSemanticVariant::Event( + RadrootsPhase1PublicationEventVariant::Time, + ), + "social.calendar_time_event.build_authored_draft", + "radroots.calendar.time_event.v1", + 31_923, + 1, + ), + ( + RadrootsPhase1PublicationSemanticVariant::FoodAvailability, + "food_availability.build_authored_draft", + "radroots.food.availability.v1", + 30_402, + 1, + ), + ]; + + for (artifact, (variant, operation, contract, kind, media_count)) in + artifacts.iter().zip(expected) + { + assert_eq!(artifact.semantic_variant(), variant); + assert_eq!(artifact.authored_operation_id(), operation); + assert_eq!(artifact.event_contract_id(), contract); + assert_eq!(artifact.expected_author().as_str(), AUTHOR); + assert_eq!(artifact.draft().kind(), kind); + assert_eq!(artifact.media_references().len(), media_count); + let canonical_json = artifact.to_canonical_json(); + assert!(canonical_json.len() <= RADROOTS_PHASE1_PUBLICATION_ARTIFACT_MAX_BYTES); + let reloaded = + RadrootsPhase1PublicationArtifact::from_canonical_json(&canonical_json).unwrap(); + assert_eq!(&reloaded, artifact); + assert_eq!(reloaded.to_canonical_json(), canonical_json); + validate_phase1_publication_artifact(artifact).unwrap(); + } +} + +#[test] +fn publication_artifact_inventory_uses_full_urls_and_includes_ask_fallbacks() { + let ask = &all_artifacts()[3]; + let urls = ask + .media_references() + .iter() + .map(|reference| reference.url().as_str()) + .collect::<Vec<_>>(); + assert_eq!( + urls, + vec![ + "https://backup.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp", + "https://media.example/51bcf96cda2475475246e3a994aabfee7ff9d7694ba0db9bdc74408632827ad0.webp", + ] + ); + assert_eq!( + ask.media_references()[0].sha256(), + ask.media_references()[1].sha256() + ); +} + +#[test] +fn publication_artifact_accepts_text_only_ask() { + let ask = RadrootsAuthoredAsk::new("When will the carrots be ready?", Vec::new()).unwrap(); + let artifact = RadrootsPhase1PublicationArtifact::from_ask(&ask, CREATED_AT, AUTHOR).unwrap(); + assert!(artifact.media_references().is_empty()); + assert_eq!( + artifact.semantic_variant(), + RadrootsPhase1PublicationSemanticVariant::Ask + ); + assert_eq!( + RadrootsPhase1PublicationArtifact::from_canonical_json(&artifact.to_canonical_json()) + .unwrap(), + artifact + ); +} + +#[test] +fn publication_artifact_reload_rejects_cross_variant_and_every_envelope_tamper() { + let artifact = &all_artifacts()[0]; + let canonical = artifact.to_canonical_json(); + + let mut leading_space = vec![b' ']; + leading_space.extend_from_slice(&canonical); + assert_error( + &leading_space, + RadrootsPhase1PublicationArtifactError::NonCanonicalJson, + ); + + for (field, replacement, expected) in [ + ( + "schema_version", + Value::from(2), + RadrootsPhase1PublicationArtifactError::UnsupportedSchemaVersion { + expected: 1, + actual: 2, + }, + ), + ( + "semantic_variant", + Value::from("update"), + RadrootsPhase1PublicationArtifactError::AuthoredOperationMismatch, + ), + ( + "authored_operation_id", + Value::from("social.update.build_authored_draft"), + RadrootsPhase1PublicationArtifactError::AuthoredOperationMismatch, + ), + ( + "event_contract_id", + Value::from("radroots.social.update.v1"), + RadrootsPhase1PublicationArtifactError::EventContractMismatch, + ), + ( + "expected_author", + Value::from("not-a-key"), + RadrootsPhase1PublicationArtifactError::InvalidExpectedAuthor, + ), + ( + "artifact_digest", + Value::from("00".repeat(32)), + RadrootsPhase1PublicationArtifactError::DigestMismatch, + ), + ] { + let mut value: Value = serde_json::from_slice(&canonical).unwrap(); + value[field] = replacement; + assert_error(&serde_json::to_vec(&value).unwrap(), expected); + } + + let mut value: Value = serde_json::from_slice(&canonical).unwrap(); + value["draft"]["content"] = Value::from("changed"); + assert_error( + &serde_json::to_vec(&value).unwrap(), + RadrootsPhase1PublicationArtifactError::ExpectedEventIdMismatch, + ); + + let mut value: Value = serde_json::from_slice(&canonical).unwrap(); + value["expected_event_id"] = Value::from("00".repeat(32)); + assert_error( + &serde_json::to_vec(&value).unwrap(), + RadrootsPhase1PublicationArtifactError::ExpectedEventIdMismatch, + ); + + let mut value: Value = serde_json::from_slice(&canonical).unwrap(); + value["unknown"] = Value::Bool(true); + assert_error( + &serde_json::to_vec(&value).unwrap(), + RadrootsPhase1PublicationArtifactError::InvalidJson, + ); +} + +#[test] +fn publication_artifact_envelope_uses_the_exact_contract_field_order() { + let canonical = String::from_utf8(all_artifacts()[0].to_canonical_json()).unwrap(); + let fields = [ + "\"schema_version\"", + "\"semantic_variant\"", + "\"authored_operation_id\"", + "\"event_contract_id\"", + "\"expected_author\"", + "\"draft\"", + "\"expected_event_id\"", + "\"media_references\"", + "\"artifact_digest\"", + ]; + let positions = fields.map(|field| { + canonical + .find(field) + .unwrap_or_else(|| panic!("missing {field}")) + }); + assert!(positions.windows(2).all(|pair| pair[0] < pair[1])); + + let draft_start = canonical.find("\"draft\":{").unwrap(); + let draft_end = canonical[draft_start..] + .find("},\"expected_event_id\"") + .map(|offset| draft_start + offset) + .unwrap(); + let draft = &canonical[draft_start..draft_end]; + let draft_positions = ["\"created_at\"", "\"kind\"", "\"tags\"", "\"content\""].map(|field| { + draft + .find(field) + .unwrap_or_else(|| panic!("missing {field}")) + }); + assert!(draft_positions.windows(2).all(|pair| pair[0] < pair[1])); + let media = &canonical[positions[7]..positions[8]]; + let media_positions = ["\"url\"", "\"sha256\"", "\"size\"", "\"media_type\""].map(|field| { + media + .find(field) + .unwrap_or_else(|| panic!("missing {field}")) + }); + assert!(media_positions.windows(2).all(|pair| pair[0] < pair[1])); + let value: Value = serde_json::from_str(&canonical).unwrap(); + assert!(value["expected_event_id"].is_string()); +} + +#[test] +fn publication_artifact_reload_rejects_media_order_and_commitment_tamper() { + let artifact = &all_artifacts()[0]; + let mut value: Value = serde_json::from_slice(&artifact.to_canonical_json()).unwrap(); + value["media_references"].as_array_mut().unwrap().reverse(); + assert_error( + &serde_json::to_vec(&value).unwrap(), + RadrootsPhase1PublicationArtifactError::NonCanonicalMediaInventory, + ); + + let mut value: Value = serde_json::from_slice(&artifact.to_canonical_json()).unwrap(); + value["media_references"][0]["size"] = Value::from(999); + assert_error( + &serde_json::to_vec(&value).unwrap(), + RadrootsPhase1PublicationArtifactError::DigestMismatch, + ); + + let photo = &all_artifacts()[2]; + let mut value: Value = serde_json::from_slice(&photo.to_canonical_json()).unwrap(); + value["media_references"][0]["size"] = Value::from(999); + assert_error( + &serde_json::to_vec(&value).unwrap(), + RadrootsPhase1PublicationArtifactError::MediaInventoryMismatch, + ); +} + +#[test] +fn publication_artifact_decode_is_bounded_before_json_parsing() { + let exact = vec![b' '; RADROOTS_PHASE1_PUBLICATION_ARTIFACT_MAX_BYTES]; + assert_eq!( + RadrootsPhase1PublicationArtifact::from_canonical_json(&exact).unwrap_err(), + RadrootsPhase1PublicationArtifactError::InvalidJson + ); + + let bytes = vec![b' '; RADROOTS_PHASE1_PUBLICATION_ARTIFACT_MAX_BYTES + 1]; + assert_eq!( + RadrootsPhase1PublicationArtifact::from_canonical_json(&bytes).unwrap_err(), + RadrootsPhase1PublicationArtifactError::ArtifactTooLarge { + max: RADROOTS_PHASE1_PUBLICATION_ARTIFACT_MAX_BYTES, + actual: bytes.len(), + } + ); +} + +#[test] +fn publication_artifact_media_count_accepts_exact_limit_and_rejects_one_over() { + let artifact = &all_artifacts()[1]; + let mut value: Value = serde_json::from_slice(&artifact.to_canonical_json()).unwrap(); + let sha256 = "11".repeat(32); + let references = (0..RADROOTS_PHASE1_PUBLICATION_MEDIA_MAX_COUNT) + .map(|index| { + serde_json::json!({ + "url": format!("https://media-{index:04}.example/{sha256}.png"), + "sha256": sha256.clone(), + "size": 1, + "media_type": "image/png" + }) + }) + .collect::<Vec<_>>(); + value["media_references"] = Value::Array(references.clone()); + assert_error( + &serde_json::to_vec(&value).unwrap(), + RadrootsPhase1PublicationArtifactError::InvalidPostProfile, + ); + + let mut one_over = references; + one_over.push(serde_json::json!({ + "url": format!("https://media-4096.example/{sha256}.png"), + "sha256": sha256, + "size": 1, + "media_type": "image/png" + })); + value["media_references"] = Value::Array(one_over); + assert_error( + &serde_json::to_vec(&value).unwrap(), + RadrootsPhase1PublicationArtifactError::TooManyMediaReferences { + max: RADROOTS_PHASE1_PUBLICATION_MEDIA_MAX_COUNT, + actual: RADROOTS_PHASE1_PUBLICATION_MEDIA_MAX_COUNT + 1, + }, + ); +} + +#[test] +fn publication_artifact_reload_requires_extensions_for_primary_media() { + let artifacts = all_artifacts(); + for (index, host) in [ + (0, "media.example"), + (2, "media.example"), + (4, "events.example"), + (6, "food.example"), + ] { + let mut value: Value = + serde_json::from_slice(&artifacts[index].to_canonical_json()).unwrap(); + let reference = value["media_references"] + .as_array_mut() + .unwrap() + .iter_mut() + .find(|reference| reference["url"].as_str().unwrap().contains(host)) + .unwrap(); + let original = reference["url"].as_str().unwrap().to_string(); + let extension_start = original.rfind('.').unwrap(); + let extensionless = original[..extension_start].to_string(); + reference["url"] = Value::from(extensionless.clone()); + replace_json_string(&mut value["draft"], &original, &extensionless); + rebuild_expected_event_id(&mut value); + assert_error( + &serde_json::to_vec(&value).unwrap(), + RadrootsPhase1PublicationArtifactError::InvalidMediaReference, + ); + } +} + +#[test] +fn publication_artifact_reload_accepts_extensionless_post_fallback() { + let image = authored_image( + b"extensionless-fallback", + "media.example", + "webp", + "image/webp", + ); + let hash = image.descriptor().sha256(); + let fallback = RadrootsBlossomBlobUrl::parse(&format!("https://backup.example/{hash}")) + .unwrap() + .approve() + .unwrap(); + let post_image = RadrootsAuthoredPostImage::new( + image, + RadrootsPostImageDimensions::new(1200, 900).unwrap(), + "Fresh strawberries", + ) + .unwrap() + .try_with_fallback(fallback) + .unwrap(); + let url = post_image.url().to_string(); + let ask = + RadrootsAuthoredAsk::new(format!("Available this week? {url}"), vec![post_image]).unwrap(); + let artifact = RadrootsPhase1PublicationArtifact::from_ask(&ask, CREATED_AT, AUTHOR).unwrap(); + assert_eq!( + RadrootsPhase1PublicationArtifact::from_canonical_json(&artifact.to_canonical_json()) + .unwrap(), + artifact + ); +} + +fn assert_error(bytes: &[u8], expected: RadrootsPhase1PublicationArtifactError) { + assert_eq!( + RadrootsPhase1PublicationArtifact::from_canonical_json(bytes).unwrap_err(), + expected + ); +} + +fn assert_every_cross_variant_is_rejected( + artifacts: &[RadrootsPhase1PublicationArtifact], + expected_error: &str, + case_id: &str, +) { + let variants = [ + "profile", + "update", + "photo_update", + "ask", + "event_date", + "event_time", + "food_availability", + ]; + let mut executed = 0usize; + for artifact in artifacts { + for target in variants { + if target == artifact.semantic_variant().as_str() { + continue; + } + let mut value: Value = serde_json::from_slice(&artifact.to_canonical_json()).unwrap(); + value["semantic_variant"] = Value::from(target); + let error = RadrootsPhase1PublicationArtifact::from_canonical_json( + &serde_json::to_vec(&value).unwrap(), + ) + .unwrap_err(); + assert_eq!(error.code(), expected_error, "{case_id}: {target}"); + executed += 1; + } + } + assert_eq!(executed, 42, "{case_id}"); +} + +fn artifact_fixture<'a>( + artifacts: &'a [RadrootsPhase1PublicationArtifact], + fixture: &str, +) -> &'a RadrootsPhase1PublicationArtifact { + let index = match fixture { + "profile" => 0, + "update" => 1, + "photo_update" => 2, + "ask" => 3, + "event_date" => 4, + "event_time" => 5, + "food_availability" => 6, + other => panic!("unknown publication fixture {other}"), + }; + &artifacts[index] +} + +fn mutate_artifact(canonical: &[u8], mutation: &str) -> Vec<u8> { + match mutation { + "leading_whitespace" => { + let mut bytes = vec![b' ']; + bytes.extend_from_slice(canonical); + return bytes; + } + "artifact_exact_byte_limit" => { + return vec![b' '; RADROOTS_PHASE1_PUBLICATION_ARTIFACT_MAX_BYTES]; + } + "artifact_one_over_byte_limit" => { + return vec![b' '; RADROOTS_PHASE1_PUBLICATION_ARTIFACT_MAX_BYTES + 1]; + } + "duplicate_expected_event_id" => { + return duplicate_expected_event_id(canonical); + } + _ => {} + } + let mut value: Value = serde_json::from_slice(canonical).unwrap(); + match mutation { + "unknown_field" => value["unknown"] = Value::Bool(true), + "unknown_draft_field" => value["draft"]["unknown"] = Value::Bool(true), + "nested_expected_event_id" => { + value["draft"]["expected_event_id"] = value["expected_event_id"].clone(); + } + "missing_expected_event_id" => { + value.as_object_mut().unwrap().remove("expected_event_id"); + } + "malformed_expected_event_id" => { + value["expected_event_id"] = Value::from("not-an-event-id"); + } + "uppercase_expected_event_id" => { + value["expected_event_id"] = Value::from( + value["expected_event_id"] + .as_str() + .unwrap() + .to_ascii_uppercase(), + ); + } + "unknown_media_field" => value["media_references"][0]["unknown"] = Value::Bool(true), + "json_field_order" => {} + "schema_version" => value["schema_version"] = Value::from(2), + "operation_id" => { + value["authored_operation_id"] = Value::from("social.update.build_authored_draft"); + } + "contract_id" => { + value["event_contract_id"] = Value::from("radroots.social.update.v1"); + } + "kind" => value["draft"]["kind"] = Value::from(1), + "author" => value["expected_author"] = Value::from("not-a-key"), + "created_at" => value["draft"]["created_at"] = Value::from(CREATED_AT + 1), + "draft_tags" => value["draft"]["tags"] = serde_json::json!([["x"]]), + "draft_content" => value["draft"]["content"] = Value::from("changed"), + "noncanonical_nip05" => { + let content = value["draft"]["content"] + .as_str() + .unwrap() + .replace("farm@example.com", "farm@EXAMPLE.COM"); + assert!(content.contains("farm@EXAMPLE.COM")); + value["draft"]["content"] = Value::from(content); + rebuild_expected_event_id(&mut value); + } + "empty_ask_content" => { + value["draft"]["content"] = Value::from(" \t"); + rebuild_expected_event_id(&mut value); + } + "expected_event_id" => { + value["expected_event_id"] = Value::from("00".repeat(32)); + } + "digest" => value["artifact_digest"] = Value::from("00".repeat(32)), + "media_order" => value["media_references"].as_array_mut().unwrap().reverse(), + "media_size" => value["media_references"][0]["size"] = Value::from(999), + "media_url" => { + let url = value["media_references"][0]["url"].as_str().unwrap(); + value["media_references"][0]["url"] = + Value::from(url.replacen("https://media.example", "https://alternate.example", 1)); + } + "media_url_casing" => { + let url = value["media_references"][0]["url"].as_str().unwrap(); + value["media_references"][0]["url"] = + Value::from(url.replacen("https://", "HTTPS://", 1)); + } + "media_hash" => value["media_references"][0]["sha256"] = Value::from("00".repeat(32)), + "media_type" => value["media_references"][0]["media_type"] = Value::from("image/jpeg"), + other => panic!("unknown publication mutation {other}"), + } + serde_json::to_vec(&value).unwrap() +} + +fn duplicate_expected_event_id(canonical: &[u8]) -> Vec<u8> { + let value: Value = serde_json::from_slice(canonical).unwrap(); + let event_id = value["expected_event_id"].as_str().unwrap(); + let field = format!("\"expected_event_id\":\"{event_id}\""); + let duplicate = format!("{field},{field}"); + let canonical = core::str::from_utf8(canonical).unwrap(); + let mutated = canonical.replacen(&field, &duplicate, 1); + assert_ne!(mutated, canonical); + mutated.into_bytes() +} + +fn rebuild_expected_event_id(value: &mut Value) { + let author = value["expected_author"].as_str().unwrap(); + let created_at = value["draft"]["created_at"].as_u64().unwrap(); + let kind = value["draft"]["kind"].as_u64().unwrap() as u32; + let tags: Vec<Vec<String>> = serde_json::from_value(value["draft"]["tags"].clone()).unwrap(); + let content = value["draft"]["content"].as_str().unwrap(); + value["expected_event_id"] = Value::from( + compute_canonical_nip01_event_id(author, created_at, kind, &tags, content) + .unwrap() + .to_string(), + ); +} + +fn replace_json_string(value: &mut Value, from: &str, to: &str) { + match value { + Value::String(string) => *string = string.replace(from, to), + Value::Array(values) => { + for value in values { + replace_json_string(value, from, to); + } + } + Value::Object(values) => { + for value in values.values_mut() { + replace_json_string(value, from, to); + } + } + _ => {} + } +} + +fn all_artifacts() -> Vec<RadrootsPhase1PublicationArtifact> { + let picture = authored_image(b"profile-picture", "media.example", "png", "image/png"); + let banner = authored_image(b"profile-banner", "media.example", "webp", "image/webp"); + let profile = RadrootsAuthoredProfile::new("victoria-farm") + .unwrap() + .with_display_name("Victoria Farm") + .with_about("Seasonal produce from the Saanich Peninsula") + .with_picture(picture) + .with_banner(banner) + .with_nip05(RadrootsNip05Identifier::parse("farm@example.com").unwrap()) + .with_bot(false); + + let post_image = authored_post_image(b"ask-and-photo"); + let post_url = post_image.url().to_string(); + let photo = RadrootsAuthoredPhotoUpdate::new( + format!("Strawberries at the farm stand {post_url}"), + vec![post_image.clone()], + ) + .unwrap(); + let ask = RadrootsAuthoredAsk::new( + format!("When will strawberries be ready? {post_url}"), + vec![post_image], + ) + .unwrap(); + + let event_image = authored_image(b"farm-event", "events.example", "jpeg", "image/jpeg"); + let date = RadrootsAuthoredCalendarDateEvent::new( + "farmers-market-2026", + "Moss Street Farmers Market", + RadrootsCalendarDate::parse("2026-07-25").unwrap(), + ) + .unwrap() + .with_end(RadrootsCalendarDate::parse("2026-07-26").unwrap()) + .unwrap() + .with_description("Saturday market in Victoria") + .unwrap() + .with_locations(vec!["Victoria, BC".to_string()]) + .unwrap() + .with_image(event_image.clone()) + .unwrap(); + let time = RadrootsAuthoredCalendarTimeEvent::new( + "farm-tour-2026", + "Saanich Farm Tour", + 1_785_003_600, + ) + .unwrap() + .with_end(1_785_007_200) + .unwrap() + .with_start_tzid("America/Vancouver") + .unwrap() + .with_description("A one-hour farm tour") + .unwrap() + .with_image(event_image) + .unwrap(); + + vec![ + RadrootsPhase1PublicationArtifact::from_profile(&profile, CREATED_AT, AUTHOR).unwrap(), + RadrootsPhase1PublicationArtifact::from_update( + &RadrootsAuthoredUpdate::new("Carrots harvested today").unwrap(), + CREATED_AT, + AUTHOR, + ) + .unwrap(), + RadrootsPhase1PublicationArtifact::from_photo_update(&photo, CREATED_AT, AUTHOR).unwrap(), + RadrootsPhase1PublicationArtifact::from_ask(&ask, CREATED_AT, AUTHOR).unwrap(), + RadrootsPhase1PublicationArtifact::from_calendar_date_event(&date, CREATED_AT, AUTHOR) + .unwrap(), + RadrootsPhase1PublicationArtifact::from_calendar_time_event(&time, CREATED_AT, AUTHOR) + .unwrap(), + RadrootsPhase1PublicationArtifact::from_food_availability( + &food_details(), + CREATED_AT, + AUTHOR, + ) + .unwrap(), + ] +} + +fn authored_post_image(bytes: &[u8]) -> RadrootsAuthoredPostImage { + let image = authored_image(bytes, "media.example", "webp", "image/webp"); + let hash = image.descriptor().sha256(); + let fallback = RadrootsBlossomBlobUrl::parse(&format!("https://backup.example/{hash}.webp")) + .unwrap() + .approve() + .unwrap(); + RadrootsAuthoredPostImage::new( + image, + RadrootsPostImageDimensions::new(1200, 900).unwrap(), + "Fresh strawberries", + ) + .unwrap() + .try_with_fallback(fallback) + .unwrap() +} + +fn food_details() -> RadrootsFoodAvailabilityDetails { + let image = RadrootsFoodAvailabilityImage::new( + authored_image(b"nantes-carrots", "food.example", "png", "image/png"), + RadrootsFoodImageDimensions::new(1200, 800).unwrap(), + ); + RadrootsFoodAvailabilityDetails::new(RadrootsFoodAvailabilityDetailsParts { + content: RadrootsFoodContent::new("Fresh Nantes carrots available this week.").unwrap(), + identifier: RadrootsFoodIdentifier::parse("nantes-carrots").unwrap(), + title: RadrootsFoodText::new("Nantes Carrots").unwrap(), + summary: RadrootsFoodText::new("Fresh bunches").unwrap(), + published_at: RadrootsFoodPublishedAt::new(CREATED_AT - 60).unwrap(), + location: RadrootsFoodText::new("Central Saanich, BC").unwrap(), + price: RadrootsFoodPrice::new( + "3", + RadrootsFoodCurrency::parse("CAD").unwrap(), + RadrootsFoodUnit::Pound, + ) + .unwrap(), + quantity: Some(RadrootsFoodQuantity::new("24", RadrootsFoodUnit::Pound).unwrap()), + status: RadrootsFoodAvailabilityStatus::Active, + images: vec![image], + }) + .unwrap() +} + +fn authored_image( + bytes: &[u8], + host: &str, + extension: &str, + media_type: &str, +) -> RadrootsAuthoredImage { + RadrootsAuthoredImage::try_from(verified_descriptor(bytes, host, extension, media_type)) + .unwrap() +} + +fn verified_descriptor( + bytes: &[u8], + host: &str, + extension: &str, + media_type: &str, +) -> RadrootsBlossomByteVerifiedDescriptor { + let sha256 = RadrootsBlossomSha256::digest(bytes); + let media_type = RadrootsBlossomMediaType::parse(media_type).unwrap(); + RadrootsBlossomBlobDescriptor::new( + RadrootsBlossomBlobUrl::parse(&format!("https://{host}/{sha256}.{extension}")).unwrap(), + sha256, + bytes.len() as u64, + media_type.clone(), + CREATED_AT, + ) + .unwrap() + .approve_reference() + .unwrap() + .verify_bytes(bytes, &media_type) + .unwrap() +} 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_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. +#[allow(dead_code)] mod raw_source_rebuild; mod registry_v7; mod source_maintenance; @@ -16,8 +20,8 @@ pub(crate) use food_availability_projection::{ pub(crate) use nip09_reconciliation::{ validate_nip09_reconciliation_manifest, write_nip09_reconciliation_manifest, }; -pub(crate) use raw_source_rebuild::{ - validate_raw_source_rebuild_manifest, write_raw_source_rebuild_manifest, +pub(crate) use phase1_publication_artifact::{ + validate_phase1_publication_artifact_manifest, write_phase1_publication_artifact_manifest, }; pub(crate) use registry_v7::{ validate_event_contract_registry_v7_inventory, write_event_contract_registry_v7_inventory, @@ -54,9 +58,19 @@ 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_knowledge_contract_manifest(workspace_root) } +pub(crate) fn validate_raw_source_rebuild_manifest(workspace_root: &Path) -> Result<(), String> { + phase1_publication_artifact::validate_immutable_raw_source_rebuild_predecessor(workspace_root) +} + +pub(crate) fn write_raw_source_rebuild_manifest(workspace_root: &Path) -> Result<(), String> { + phase1_publication_artifact::validate_immutable_raw_source_rebuild_predecessor(workspace_root)?; + Err("raw-source rebuild is an immutable predecessor and cannot be rewritten; write the active publication successor instead".to_owned()) +} + const ROOT_RELEASE_POLICY_RELATIVE: &str = "foundation/contracts/release_runtime/mounted_rust_crates/publish-policy.toml"; const CONFORMANCE_ROOT_RELATIVE: &str = "contracts/conformance"; @@ -94,7 +108,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); 23] = [ +const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 24] = [ ( "contracts/conformance/vectors/blossom/bud11_claims.v1.json", "crates/blossom/tests/fixtures/bud11_claims.v1.json", @@ -180,6 +194,10 @@ const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 23] = [ "crates/event_codec/tests/fixtures/post_verified_profiles.v1.json", ), ( + "contracts/conformance/vectors/publication/phase1_artifact.v1.json", + "crates/event_codec/tests/fixtures/phase1_publication_artifact.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/nip09_reconciliation.rs b/tools/xtask/src/contract/nip09_reconciliation.rs @@ -5312,7 +5312,12 @@ fn validate_raw_crate_attributes(relative: &str, source: &str) -> Result<(), Str "#![cfg_attr(all(not(feature=\"std\"),not(test)),no_std)]", "#![forbid(unsafe_code)]", ], - "crates/event_codec/src/lib.rs" | "crates/blossom/src/lib.rs" => &[ + "crates/event_codec/src/lib.rs" => &[ + "#![cfg_attr(all(not(feature=\"std\"),not(test)),no_std)]", + "#![cfg_attr(coverage_nightly,feature(coverage_attribute))]", + "#![forbid(unsafe_code)]", + ], + "crates/blossom/src/lib.rs" => &[ "#![cfg_attr(not(feature=\"std\"),no_std)]", "#![cfg_attr(coverage_nightly,feature(coverage_attribute))]", "#![forbid(unsafe_code)]", @@ -17323,6 +17328,37 @@ mod tests { let predecessor = toml::to_string_pretty(&manifest).expect("serialize predecessor Cargo manifest"); fs::write(manifest_path, predecessor).expect("restore predecessor compiler manifest"); + + let codec_manifest_path = workspace_root.join(EVENT_CODEC_CARGO_MANIFEST_RELATIVE); + let codec_source = + fs::read_to_string(&codec_manifest_path).expect("event-codec Cargo manifest"); + let mut codec_manifest: toml::Value = + toml::from_str(&codec_source).expect("parse event-codec Cargo manifest"); + let serde_json_features = codec_manifest + .get_mut("features") + .and_then(toml::Value::as_table_mut) + .and_then(|features| features.get_mut("serde_json")) + .and_then(toml::Value::as_array_mut) + .expect("event-codec serde_json feature"); + assert_eq!( + serde_json_features + .iter() + .map(toml::Value::as_str) + .collect::<Vec<_>>(), + [ + Some("serde"), + Some("dep:hex"), + Some("dep:serde_json"), + Some("dep:sha2"), + ], + "publication successor feature edges must retain their exact semantic shape" + ); + serde_json_features + .retain(|feature| !matches!(feature.as_str(), Some("dep:hex" | "dep:sha2"))); + let codec_predecessor = toml::to_string_pretty(&codec_manifest) + .expect("serialize predecessor event-codec Cargo manifest"); + fs::write(codec_manifest_path, codec_predecessor) + .expect("restore predecessor event-codec compiler manifest"); } fn strip_outer_try(statement: &mut syn::Stmt) { diff --git a/tools/xtask/src/contract/phase1_publication_artifact.rs b/tools/xtask/src/contract/phase1_publication_artifact.rs @@ -0,0 +1,1819 @@ +use super::artifact_bundle::{ + GeneratedArtifact, read_regular_file, with_artifact_bundle_transaction, +}; +use radroots_event_codec::wire::publication::{ + RADROOTS_PHASE1_PUBLICATION_ARTIFACT_MAX_BYTES, + RADROOTS_PHASE1_PUBLICATION_ARTIFACT_SCHEMA_VERSION, + RADROOTS_PHASE1_PUBLICATION_MEDIA_MAX_COUNT, + RADROOTS_PHASE1_PUBLICATION_SIGNED_EVENT_MAX_BYTES, +}; +use serde::{Deserialize, Serialize}; +use serde_json::{Value, json}; +use sha2::{Digest, Sha256}; +use std::collections::{BTreeMap, BTreeSet}; +use std::fs; +use std::path::Path; + +const SCHEMA_VERSION: u32 = 1; +const CONTRACT_ID: &str = "radroots_event_codec.phase1_publication_artifact_v1"; +const AUTHORITY_ID: &str = "phase1_publication_artifact_v1"; +const HASH_ALGORITHM: &str = "sha256_bytes_v1"; +const WRITE_COMMAND: &str = "cargo xtask contract phase1-publication-artifact-manifest --write"; + +const MANIFEST_RELATIVE: &str = + "crates/event_codec/contracts/phase1_publication_artifact_v1.manifest.json"; +const MANIFEST_SCHEMA_RELATIVE: &str = + "crates/event_codec/contracts/phase1_publication_artifact_v1.manifest.schema.json"; +const MANIFEST_SHA256_RELATIVE: &str = + "crates/event_codec/contracts/phase1_publication_artifact_v1.manifest.sha256"; +const VECTOR_RELATIVE: &str = "contracts/conformance/vectors/publication/phase1_artifact.v1.json"; +const VECTOR_MIRROR_RELATIVE: &str = + "crates/event_codec/tests/fixtures/phase1_publication_artifact.v1.json"; +const VECTOR_EXECUTOR_RELATIVE: &str = "crates/event_codec/tests/publication_artifact.rs"; +const VECTOR_EXECUTOR_TEST: &str = "publication_artifact_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-artifact"; +const CHANGELOG_MARKER: &str = "<!-- release-change: phase1-publication-artifact -->"; +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 RAW_MANIFEST_RELATIVE: &str = + "crates/event_store/contracts/raw_source_rebuild_v1.manifest.json"; +const RAW_MANIFEST_SCHEMA_RELATIVE: &str = + "crates/event_store/contracts/raw_source_rebuild_v1.manifest.schema.json"; +const RAW_MANIFEST_SHA256_RELATIVE: &str = + "crates/event_store/contracts/raw_source_rebuild_v1.manifest.sha256"; +const RAW_GENERATED_DESCRIPTOR_RELATIVE: &str = + "crates/event_store/src/generated/raw_source_rebuild_manifest.rs"; +const RAW_VECTOR_RELATIVE: &str = + "contracts/conformance/vectors/event_store/raw_source_rebuild.v1.json"; +const RAW_VECTOR_MIRROR_RELATIVE: &str = + "crates/event_store/tests/fixtures/raw_source_rebuild.v1.json"; +const RAW_VECTOR_EXECUTOR_RELATIVE: &str = + "crates/event_store/tests/raw_source_rebuild_v1_result_vector.rs"; + +const GENERATED_ARTIFACT_PATHS: &[&str] = &[ + MANIFEST_RELATIVE, + MANIFEST_SCHEMA_RELATIVE, + MANIFEST_SHA256_RELATIVE, + VECTOR_MIRROR_RELATIVE, +]; + +const PUBLIC_SOURCE_ROOTS: &[&str] = &[ + "crates/blossom/src", + "crates/core/src", + "crates/event/src", + "crates/event_codec/src", + "crates/transport/src", +]; + +const EXPLICIT_SOURCE_SPECS: &[(&str, &str)] = &[ + ("cargo_config_authority", ".cargo/config.toml"), + ("workspace_manifest_authority", "Cargo.toml"), + ("workspace_lockfile_authority", "Cargo.lock"), + ("rust_toolchain_authority", "rust-toolchain.toml"), + ("blossom_manifest_authority", "crates/blossom/Cargo.toml"), + ("core_manifest_authority", "crates/core/Cargo.toml"), + ("event_manifest_authority", "crates/event/Cargo.toml"), + ( + "event_codec_manifest_authority", + "crates/event_codec/Cargo.toml", + ), + ("event_codec_documentation", "crates/event_codec/README"), + ( + "event_store_manifest_authority", + "crates/event_store/Cargo.toml", + ), + ( + "transport_manifest_authority", + "crates/transport/Cargo.toml", + ), + ("xtask_manifest_authority", "tools/xtask/Cargo.toml"), + ("publication_vector_executor", VECTOR_EXECUTOR_RELATIVE), + ("operations_authority", OPERATIONS_RELATIVE), + ("release_authority", RELEASE_RELATIVE), + ("release_notes", CHANGELOG_RELATIVE), + ("contract_command_authority", "tools/xtask/src/contract.rs"), + ("contract_dispatch_authority", "tools/xtask/src/main.rs"), + ( + "superseded_nip09_contract_governance", + "tools/xtask/src/contract/nip09_reconciliation.rs", + ), + ( + "superseded_raw_rebuild_contract_governance", + "tools/xtask/src/contract/raw_source_rebuild.rs", + ), + ( + "publication_contract_governance", + "tools/xtask/src/contract/phase1_publication_artifact.rs", + ), +]; + +const RAW_PREDECESSOR_SUPERSEDED_PATHS: &[&str] = &[ + CHANGELOG_RELATIVE, + RELEASE_RELATIVE, + "tools/xtask/src/contract.rs", + "tools/xtask/src/main.rs", + "tools/xtask/src/contract/nip09_reconciliation.rs", + "tools/xtask/src/contract/raw_source_rebuild.rs", +]; + +const PUBLIC_TYPES: &[&str] = &[ + "RadrootsPhase1PublicationEventVariant", + "RadrootsPhase1PublicationSemanticVariant", + "RadrootsPhase1PublicationDraft", + "RadrootsPhase1PublicationMediaReference", + "RadrootsPhase1PublicationArtifactDigest", + "RadrootsPhase1PublicationArtifact", + "RadrootsPhase1PublicationArtifactError", +]; + +const VALID_VECTOR_CASES: &[(&str, &str)] = &[ + ( + "profile_round_trip", + "publication_artifact.build_profile.valid", + ), + ( + "update_round_trip", + "publication_artifact.build_update.valid", + ), + ( + "photo_update_round_trip", + "publication_artifact.build_photo_update.valid", + ), + ( + "ask_round_trip_with_fallback", + "publication_artifact.build_ask.valid", + ), + ( + "event_date_round_trip", + "publication_artifact.build_calendar_date_event.valid", + ), + ( + "event_time_round_trip", + "publication_artifact.build_calendar_time_event.valid", + ), + ( + "food_availability_round_trip", + "publication_artifact.build_food_availability.valid", + ), + ( + "canonical_json_serialization", + "publication_artifact.to_canonical_json.valid", + ), + ( + "canonical_json_reload", + "publication_artifact.from_canonical_json.valid", + ), +]; + +const INVALID_VECTOR_KIND: &str = "publication_artifact.from_canonical_json.invalid"; + +const REQUIRED_INVALID_VECTOR_IDS: &[&str] = &[ + "leading_whitespace_is_noncanonical", + "artifact_exact_byte_limit_reaches_parser", + "artifact_one_byte_over_limit_is_rejected", + "unknown_field_is_rejected", + "unknown_draft_field_is_rejected", + "nested_expected_event_id_is_rejected", + "missing_expected_event_id_is_rejected", + "malformed_expected_event_id_is_rejected", + "uppercase_expected_event_id_is_rejected", + "duplicate_expected_event_id_is_rejected", + "unknown_media_field_is_rejected", + "json_field_order_is_noncanonical", + "unknown_version_is_rejected", + "cross_variant_is_rejected", + "operation_mismatch_is_rejected", + "contract_mismatch_is_rejected", + "kind_mismatch_is_rejected", + "author_mismatch_is_rejected", + "created_at_tamper_is_rejected", + "draft_tags_tamper_is_rejected", + "draft_content_tamper_is_rejected", + "noncanonical_nip05_is_rejected_after_id_rebuild", + "empty_ask_content_is_rejected_after_id_rebuild", + "expected_event_id_tamper_is_rejected", + "digest_tamper_is_rejected", + "media_order_is_rejected", + "profile_media_commitment_tamper_is_rejected", + "media_url_tamper_is_rejected", + "noncanonical_media_url_casing_is_rejected", + "media_hash_tamper_is_rejected", + "media_type_tamper_is_rejected", + "post_media_commitment_must_match_imeta", +]; + +const RAW_IMMUTABLE_ARTIFACTS: &[ImmutableArtifactSpec] = &[ + ImmutableArtifactSpec::new( + RAW_MANIFEST_RELATIVE, + 45_449, + "03253ce31dc31d465880a895d2685f5deb1274948e0a4eabe81a2f08f238c483", + ), + ImmutableArtifactSpec::new( + RAW_MANIFEST_SCHEMA_RELATIVE, + 17_896, + "f9d210967e54b66f39c8bb965d97b2001a0ebc0927e7c2c14edb8e474bfda695", + ), + ImmutableArtifactSpec::new( + RAW_MANIFEST_SHA256_RELATIVE, + 65, + "2b8bc07cd479be2281781660efd26fd7a8f480e5f3f62053aeccd2b5e6b2070c", + ), + ImmutableArtifactSpec::new( + RAW_GENERATED_DESCRIPTOR_RELATIVE, + 50_735, + "3763fbee3ee45621afca990002b9298c791bf0396ebf7bccde0ae1bc9aecb7f2", + ), + ImmutableArtifactSpec::new( + RAW_VECTOR_RELATIVE, + 26_833, + "c37a2bf3714f53ab04fae8c5c9dbe2ad4b3f5310efa51f46bd8b116660f1fe15", + ), + ImmutableArtifactSpec::new( + RAW_VECTOR_MIRROR_RELATIVE, + 26_833, + "c37a2bf3714f53ab04fae8c5c9dbe2ad4b3f5310efa51f46bd8b116660f1fe15", + ), + ImmutableArtifactSpec::new( + RAW_VECTOR_EXECUTOR_RELATIVE, + 25_542, + "51647259efdd0d99689ef1db0defb139c8d1f60f2ead69b793ddb2733a28e832", + ), +]; + +const GOVERNED_COMPILER_TABLES: &[(&str, &str, &str)] = &[ + ( + "crates/core/Cargo.toml", + "radroots_core", + "3eaa58e5c6a6e1ee857da1d519c65480a15b3432376cb14c4fa781870caef359", + ), + ( + "crates/event/Cargo.toml", + "radroots_event", + "c6908858696c9b9df86eea1983acf251aa68242ccc24fe5c8ad89c10aafd3cb3", + ), + ( + "crates/event_codec/Cargo.toml", + "radroots_event_codec", + "ee2e9c60570b1a266a6e813b758c1d556559eaf71acb16ddd8bef93591a26d4b", + ), + ( + "crates/blossom/Cargo.toml", + "radroots_blossom", + "b91985be4f3da164b434a06ab816321c1cd27a44b0ca7f817881a5ff57ff168d", + ), + ( + "crates/event_store/Cargo.toml", + "radroots_event_store", + "8afca70838e4d83a7b6409c91cef7184d805086d9fba342fd42ff2c4010db450", + ), + ( + "crates/transport/Cargo.toml", + "radroots_transport", + "a1391f424322e4851baa881c787f6b824cbc5ff69834db44e4b96e76cb776c15", + ), +]; + +const CONSTRUCTORS: &[ConstructorSpec] = &[ + ConstructorSpec { + semantic_variant: "profile", + serialized_semantic_variant: "profile", + event_variant: None, + strict_input: "RadrootsAuthoredProfile", + constructor: "from_profile", + publication_operation_id: "publication_artifact.build_profile", + authored_operation_id: "profile.build_authored_draft", + event_contract_id: "radroots.profile.metadata.v1", + kind: 0, + }, + ConstructorSpec { + semantic_variant: "update", + serialized_semantic_variant: "update", + event_variant: None, + strict_input: "RadrootsAuthoredUpdate", + constructor: "from_update", + publication_operation_id: "publication_artifact.build_update", + authored_operation_id: "social.update.build_authored_draft", + event_contract_id: "radroots.social.update.v1", + kind: 1, + }, + ConstructorSpec { + semantic_variant: "photo_update", + serialized_semantic_variant: "photo_update", + event_variant: None, + strict_input: "RadrootsAuthoredPhotoUpdate", + constructor: "from_photo_update", + publication_operation_id: "publication_artifact.build_photo_update", + authored_operation_id: "social.photo_update.build_authored_draft", + event_contract_id: "radroots.social.photo_update.v1", + kind: 1, + }, + ConstructorSpec { + semantic_variant: "ask", + serialized_semantic_variant: "ask", + event_variant: None, + strict_input: "RadrootsAuthoredAsk", + constructor: "from_ask", + publication_operation_id: "publication_artifact.build_ask", + authored_operation_id: "social.ask.build_authored_draft", + event_contract_id: "radroots.social.ask.v1", + kind: 1, + }, + ConstructorSpec { + semantic_variant: "event", + serialized_semantic_variant: "event_date", + event_variant: Some("date"), + strict_input: "RadrootsAuthoredCalendarDateEvent", + constructor: "from_calendar_date_event", + publication_operation_id: "publication_artifact.build_calendar_date_event", + authored_operation_id: "social.calendar_date_event.build_authored_draft", + event_contract_id: "radroots.calendar.date_event.v1", + kind: 31_922, + }, + ConstructorSpec { + semantic_variant: "event", + serialized_semantic_variant: "event_time", + event_variant: Some("time"), + strict_input: "RadrootsAuthoredCalendarTimeEvent", + constructor: "from_calendar_time_event", + publication_operation_id: "publication_artifact.build_calendar_time_event", + authored_operation_id: "social.calendar_time_event.build_authored_draft", + event_contract_id: "radroots.calendar.time_event.v1", + kind: 31_923, + }, + ConstructorSpec { + semantic_variant: "food_availability", + serialized_semantic_variant: "food_availability", + event_variant: None, + strict_input: "RadrootsFoodAvailabilityDetails", + constructor: "from_food_availability", + publication_operation_id: "publication_artifact.build_food_availability", + authored_operation_id: "food_availability.build_authored_draft", + event_contract_id: "radroots.food.availability.v1", + kind: 30_402, + }, +]; + +#[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 ConstructorSpec { + semantic_variant: &'static str, + serialized_semantic_variant: &'static str, + event_variant: Option<&'static str>, + strict_input: &'static str, + constructor: &'static str, + publication_operation_id: &'static str, + authored_operation_id: &'static str, + event_contract_id: &'static str, + kind: u32, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct PublicationManifest { + schema_version: u32, + contract_id: String, + authority_id: String, + manifest_schema: FileDescriptor, + predecessor: PredecessorDescriptor, + event_contract_registry: RegistryDescriptor, + artifact: ArtifactDescriptor, + operations: Vec<OperationDescriptor>, + predecessor_source_supersessions: Vec<String>, + source_files: Vec<SourceFileDescriptor>, + result_vector: ResultVectorDescriptor, + release: ReleaseDescriptor, +} + +#[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, + manifest: 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 ArtifactDescriptor { + schema_version: u32, + validator: String, + semantic_roles: Vec<String>, + serialized_semantic_variants: Vec<String>, + event_subvariants: Vec<String>, + canonical_encoding: String, + artifact_max_bytes: u64, + signed_event_wire_max_bytes: u64, + media_reference_max_count: u64, + envelope_fields: Vec<String>, + draft_fields: Vec<String>, + media_reference_fields: Vec<String>, + media_reference_identity: String, + primary_media_url_requirement: String, + post_fallback_url_requirement: String, + digest_algorithm: String, + digest_domain: String, + digest_domain_terminator: String, + digest_preimage: String, + constructors: Vec<ConstructorDescriptor>, + denied_inputs: Vec<String>, + reload_capability: String, + threat_boundary: Vec<String>, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct ConstructorDescriptor { + semantic_variant: String, + serialized_semantic_variant: String, + event_variant: Option<String>, + strict_input: String, + constructor: String, + publication_operation_id: String, + authored_operation_id: String, + event_contract_id: String, + kind: u32, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +struct OperationDescriptor { + id: String, + strict_input: String, + output: String, + error_class: String, + signing: String, + transport: 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(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 ValidatedResultVector { + valid_case_ids: Vec<String>, + invalid_case_ids: Vec<String>, + bytes: Vec<u8>, +} + +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) + }) +} + +pub(crate) fn validate_phase1_publication_artifact_manifest( + workspace_root: &Path, +) -> Result<(), String> { + with_artifact_bundle_transaction(workspace_root, |_| { + validate_manifest_under_lock(workspace_root) + }) +} + +pub(crate) fn validate_immutable_raw_source_rebuild_predecessor( + workspace_root: &Path, +) -> Result<(), String> { + with_artifact_bundle_transaction(workspace_root, |_| { + validate_immutable_raw_predecessor_under_lock(workspace_root) + }) +} + +fn validate_manifest_under_lock(workspace_root: &Path) -> Result<(), String> { + validate_immutable_raw_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 contract {} is stale; run {WRITE_COMMAND}", + artifact.relative + )); + } + } + + let bytes = read_regular_file(workspace_root, MANIFEST_RELATIVE)?; + let manifest: PublicationManifest = 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 manifest bytes" + )); + } + Ok(()) +} + +fn expected_artifacts(workspace_root: &Path) -> Result<Vec<GeneratedArtifact>, String> { + validate_immutable_raw_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 vector_bytes = read_regular_file(workspace_root, VECTOR_RELATIVE)?; + Ok(vec![ + GeneratedArtifact { + relative: MANIFEST_RELATIVE, + contents: manifest_bytes.clone(), + }, + GeneratedArtifact { + relative: MANIFEST_SCHEMA_RELATIVE, + contents: schema_bytes, + }, + GeneratedArtifact { + relative: MANIFEST_SHA256_RELATIVE, + contents: format!("{}\n", sha256_hex(&manifest_bytes)).into_bytes(), + }, + GeneratedArtifact { + relative: VECTOR_MIRROR_RELATIVE, + contents: vector_bytes, + }, + ]) +} + +fn describe_manifest( + workspace_root: &Path, + schema_bytes: &[u8], +) -> Result<PublicationManifest, String> { + let source_files = source_specs(workspace_root)? + .into_iter() + .map(|(role, path)| source_descriptor(workspace_root, &role, &path)) + .collect::<Result<Vec<_>, _>>()?; + let vector = validate_result_vector(workspace_root)?; + let vector_byte_length = byte_length(VECTOR_RELATIVE, &vector.bytes)?; + let vector_sha256 = sha256_hex(&vector.bytes); + Ok(PublicationManifest { + 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: "radroots_event_store.raw_source_rebuild_v1".to_owned(), + manifest: descriptor_for_file(workspace_root, RAW_MANIFEST_RELATIVE)?, + }, + event_contract_registry: RegistryDescriptor { + version: 7, + inventory: descriptor_for_file(workspace_root, REGISTRY_RELATIVE)?, + sidecar: descriptor_for_file(workspace_root, REGISTRY_SIDECAR_RELATIVE)?, + evolution: "immutable_registry_v7_plus_additive_publication_authority_v1".to_owned(), + }, + artifact: expected_artifact_descriptor(), + operations: expected_operation_descriptors(), + predecessor_source_supersessions: RAW_PREDECESSOR_SUPERSEDED_PATHS + .iter() + .map(|value| (*value).to_owned()) + .collect(), + source_files, + result_vector: ResultVectorDescriptor { + canonical_path: VECTOR_RELATIVE.to_owned(), + mirror_path: VECTOR_MIRROR_RELATIVE.to_owned(), + byte_length: vector_byte_length, + sha256: vector_sha256, + 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_artifact_descriptor() -> ArtifactDescriptor { + ArtifactDescriptor { + schema_version: RADROOTS_PHASE1_PUBLICATION_ARTIFACT_SCHEMA_VERSION, + validator: "validate_phase1_publication_artifact".to_owned(), + semantic_roles: [ + "profile", + "update", + "photo_update", + "ask", + "event", + "food_availability", + ] + .into_iter() + .map(str::to_owned) + .collect(), + serialized_semantic_variants: [ + "profile", + "update", + "photo_update", + "ask", + "event_date", + "event_time", + "food_availability", + ] + .into_iter() + .map(str::to_owned) + .collect(), + event_subvariants: ["date", "time"].into_iter().map(str::to_owned).collect(), + canonical_encoding: "serde_json_compact_struct_field_order_v1".to_owned(), + artifact_max_bytes: RADROOTS_PHASE1_PUBLICATION_ARTIFACT_MAX_BYTES as u64, + signed_event_wire_max_bytes: RADROOTS_PHASE1_PUBLICATION_SIGNED_EVENT_MAX_BYTES as u64, + media_reference_max_count: RADROOTS_PHASE1_PUBLICATION_MEDIA_MAX_COUNT as u64, + envelope_fields: [ + "schema_version", + "semantic_variant", + "authored_operation_id", + "event_contract_id", + "expected_author", + "draft", + "expected_event_id", + "media_references", + "artifact_digest", + ] + .into_iter() + .map(str::to_owned) + .collect(), + draft_fields: ["created_at", "kind", "tags", "content"] + .into_iter() + .map(str::to_owned) + .collect(), + media_reference_fields: ["url", "sha256", "size", "media_type"] + .into_iter() + .map(str::to_owned) + .collect(), + media_reference_identity: + "exact_case_preserving_approved_url_with_descriptor_commitment_v1".to_owned(), + primary_media_url_requirement: "blossom_hash_path_extension_required_v1".to_owned(), + post_fallback_url_requirement: "approved_blossom_url_extension_optional_v1".to_owned(), + digest_algorithm: "sha256_domain_nul_canonical_json_v1".to_owned(), + digest_domain: "radroots.phase1.publication-artifact.v1".to_owned(), + digest_domain_terminator: "0x00".to_owned(), + digest_preimage: "ascii_domain_then_single_nul_then_canonical_envelope_without_digest_v1" + .to_owned(), + constructors: CONSTRUCTORS + .iter() + .map(|spec| ConstructorDescriptor { + semantic_variant: spec.semantic_variant.to_owned(), + serialized_semantic_variant: spec.serialized_semantic_variant.to_owned(), + event_variant: spec.event_variant.map(str::to_owned), + strict_input: spec.strict_input.to_owned(), + constructor: format!("RadrootsPhase1PublicationArtifact::{}", spec.constructor), + publication_operation_id: spec.publication_operation_id.to_owned(), + authored_operation_id: spec.authored_operation_id.to_owned(), + event_contract_id: spec.event_contract_id.to_owned(), + kind: spec.kind, + }) + .collect(), + denied_inputs: [ + "arbitrary_event_draft", + "raw_json", + "numeric_kind", + "signed_event", + "private_key", + "signer", + ] + .into_iter() + .map(str::to_owned) + .collect(), + reload_capability: "persisted_artifact_only_no_prior_capability_restoration_v1".to_owned(), + threat_boundary: [ + "detects_accidental_corruption", + "detects_payload_only_modification", + "detects_digest_only_modification", + "does_not_authenticate_actor_rewriting_payload_and_digest", + "does_not_survive_validator_binary_or_host_compromise", + "does_not_restore_byte_verification_or_upload_completion", + "does_not_replace_nip01_id_and_signature_verification", + ] + .into_iter() + .map(str::to_owned) + .collect(), + } +} + +fn expected_operation_descriptors() -> Vec<OperationDescriptor> { + CONSTRUCTORS + .iter() + .map(|spec| OperationDescriptor { + id: spec.publication_operation_id.to_owned(), + strict_input: spec.strict_input.to_owned(), + output: "RadrootsPhase1PublicationArtifact".to_owned(), + error_class: "validation_error".to_owned(), + signing: "none".to_owned(), + transport: "none".to_owned(), + }) + .chain([ + OperationDescriptor { + id: "publication_artifact.to_canonical_json".to_owned(), + strict_input: "RadrootsPhase1PublicationArtifact".to_owned(), + output: "Bytes".to_owned(), + error_class: "none".to_owned(), + signing: "none".to_owned(), + transport: "none".to_owned(), + }, + OperationDescriptor { + id: "publication_artifact.from_canonical_json".to_owned(), + strict_input: "Bytes".to_owned(), + output: "RadrootsPhase1PublicationArtifact".to_owned(), + error_class: "parse_error".to_owned(), + signing: "none".to_owned(), + transport: "none".to_owned(), + }, + ]) + .collect() +} + +fn validate_source_contract(workspace_root: &Path) -> Result<(), String> { + validate_compiler_authority(workspace_root)?; + validate_operations_authority(workspace_root)?; + validate_release_authority(workspace_root)?; + validate_result_vector(workspace_root)?; + let paths = source_specs(workspace_root)? + .into_iter() + .map(|(_, path)| path) + .collect::<BTreeSet<_>>(); + for required in RAW_PREDECESSOR_SUPERSEDED_PATHS { + if !paths.contains(*required) { + return Err(format!( + "publication successor does not bind superseded predecessor source {required}" + )); + } + } + for generated in GENERATED_ARTIFACT_PATHS { + if paths.contains(*generated) { + return Err(format!( + "publication source inventory recursively includes generated artifact {generated}" + )); + } + } + Ok(()) +} + +fn validate_compiler_authority(workspace_root: &Path) -> Result<(), String> { + let toolchain = parse_toml(workspace_root, "rust-toolchain.toml")?; + let expected_toolchain: toml::Value = toml::from_str( + r#" +[toolchain] +channel = "1.97.0" +components = ["clippy", "rust-analyzer", "rust-src", "rustfmt"] +targets = ["wasm32-unknown-unknown"] +"#, + ) + .map_err(|error| format!("parse expected toolchain: {error}"))?; + if toolchain != expected_toolchain { + return Err("rust-toolchain.toml drifted from Rust 1.97.0 authority".to_owned()); + } + let cargo_config = parse_toml(workspace_root, ".cargo/config.toml")?; + let expected_config: toml::Value = toml::from_str("[alias]\nxtask = \"run -q -p xtask --\"\n") + .map_err(|error| format!("parse expected Cargo config: {error}"))?; + if cargo_config != expected_config { + return Err(".cargo/config.toml must remain the xtask alias only".to_owned()); + } + for forbidden in ["rust-toolchain", ".cargo/config"] { + match fs::symlink_metadata(workspace_root.join(forbidden)) { + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Ok(_) => { + return Err(format!( + "legacy compiler configuration {forbidden} must be absent" + )); + } + Err(error) => return Err(format!("inspect {forbidden}: {error}")), + } + } + + for (relative, package_name, expected_hash) in GOVERNED_COMPILER_TABLES { + let manifest = parse_toml(workspace_root, relative)?; + validate_package_shape(workspace_root, relative, package_name, &manifest)?; + let mut tables = BTreeMap::new(); + for key in ["dependencies", "features"] { + tables.insert( + key.to_owned(), + manifest + .get(key) + .ok_or_else(|| format!("{relative} must declare [{key}]"))? + .clone(), + ); + } + if *relative == "crates/event_store/Cargo.toml" { + tables.insert( + "dev-dependencies".to_owned(), + manifest + .get("dev-dependencies") + .ok_or_else(|| format!("{relative} must declare [dev-dependencies]"))? + .clone(), + ); + } + let actual = sha256_hex( + &serde_json::to_vec(&tables) + .map_err(|error| format!("serialize {relative} compiler tables: {error}"))?, + ); + if actual != *expected_hash { + return Err(format!( + "{relative} compiler tables drifted: expected {expected_hash}, found {actual}" + )); + } + } + Ok(()) +} + +fn validate_package_shape( + workspace_root: &Path, + relative: &str, + expected_name: &str, + manifest: &toml::Value, +) -> Result<(), String> { + let package = manifest + .get("package") + .and_then(toml::Value::as_table) + .ok_or_else(|| format!("{relative} must declare [package]"))?; + if package.get("name").and_then(toml::Value::as_str) != Some(expected_name) + || package.get("version").and_then(toml::Value::as_str) != Some("1.0.0-alpha.1") + || package + .get("edition") + .and_then(toml::Value::as_table) + .and_then(|value| value.get("workspace")) + .and_then(toml::Value::as_bool) + != Some(true) + || package + .get("rust-version") + .and_then(toml::Value::as_table) + .and_then(|value| value.get("workspace")) + .and_then(toml::Value::as_bool) + != Some(true) + { + return Err(format!("{relative} package/compiler identity drifted")); + } + if [ + "build", + "autolib", + "autobins", + "autoexamples", + "autotests", + "autobenches", + ] + .iter() + .any(|key| package.contains_key(*key)) + || [ + "build-dependencies", + "target", + "lib", + "bin", + "example", + "test", + "bench", + ] + .iter() + .any(|key| manifest.get(*key).is_some()) + { + return Err(format!( + "{relative} introduces unapproved build or target authority" + )); + } + let package_root = workspace_root + .join(relative) + .parent() + .ok_or_else(|| format!("{relative} has no package root"))? + .to_path_buf(); + for path in [ + package_root.join("build.rs"), + package_root.join("src/main.rs"), + package_root.join("src/bin"), + ] { + match fs::symlink_metadata(&path) { + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Ok(_) => { + return Err(format!( + "{relative} has unapproved auto-discovered target {}", + path.display() + )); + } + Err(error) => return Err(format!("inspect {}: {error}", path.display())), + } + } + Ok(()) +} + +fn validate_operations_authority(workspace_root: &Path) -> Result<(), String> { + let manifest = parse_toml(workspace_root, OPERATIONS_RELATIVE)?; + let error_classes = toml_string_array( + OPERATIONS_RELATIVE, + manifest + .get("errors") + .and_then(|value| value.get("classes")), + )?; + if error_classes + .iter() + .filter(|value| value.as_str() == "none") + .count() + != 1 + { + return Err("error classes must contain none exactly once".to_owned()); + } + let domains = toml_string_array( + OPERATIONS_RELATIVE, + manifest + .get("public") + .and_then(|value| value.get("domains")), + )?; + if domains + .iter() + .filter(|value| value.as_str() == "publication") + .count() + != 1 + { + return Err("public domains must contain publication exactly once".to_owned()); + } + let types = toml_string_array( + OPERATIONS_RELATIVE, + manifest + .get("shared_types") + .and_then(|value| value.get("public")), + )?; + for required in PUBLIC_TYPES { + if types + .iter() + .filter(|value| value.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 must declare [operations]".to_owned())?; + for forbidden in [ + "phase1_publication_artifact_build", + "phase1_publication_artifact_reload", + ] { + if operations.contains_key(forbidden) { + return Err(format!( + "obsolete publication operation {forbidden} is forbidden" + )); + } + } + for expected in expected_operation_descriptors() { + let (key, expected_inputs) = + if let Some(suffix) = expected.id.strip_prefix("publication_artifact.build_") { + ( + format!("phase1_publication_artifact_build_{suffix}"), + vec![ + expected.strict_input.clone(), + "u64".to_owned(), + "String".to_owned(), + ], + ) + } else { + match expected.id.as_str() { + "publication_artifact.to_canonical_json" => ( + "phase1_publication_artifact_to_canonical_json".to_owned(), + vec![expected.strict_input.clone()], + ), + "publication_artifact.from_canonical_json" => ( + "phase1_publication_artifact_from_canonical_json".to_owned(), + vec![expected.strict_input.clone()], + ), + other => return Err(format!("unknown publication operation {other}")), + } + }; + let operation = operations + .get(&key) + .and_then(toml::Value::as_table) + .ok_or_else(|| format!("operations.toml is missing {key}"))?; + require_toml_string(operation, "domain", "publication", &key)?; + require_toml_string(operation, "id", &expected.id, &key)?; + require_toml_string(operation, "stability", "beta", &key)?; + require_toml_string(operation, "error_class", &expected.error_class, &key)?; + require_toml_string(operation, "signing", "none", &key)?; + require_toml_string(operation, "transport", "none", &key)?; + if operation + .get("deterministic") + .and_then(toml::Value::as_bool) + != Some(true) + || toml_string_array(&key, operation.get("inputs"))? != expected_inputs + || toml_string_array(&key, operation.get("outputs"))? != [expected.output.clone()] + { + return Err(format!("{key} signature or determinism drifted")); + } + let implementation = operation + .get("implementation") + .and_then(toml::Value::as_table) + .ok_or_else(|| format!("{key} implementation is missing"))?; + let modules = toml_string_array(&key, implementation.get("rust_modules"))?; + if !modules + .iter() + .any(|path| path == "crates/event_codec/src/wire/publication.rs") + { + return Err(format!( + "{key} does not route to the sealed publication module" + )); + } + let conformance = operation + .get("conformance") + .and_then(toml::Value::as_table) + .ok_or_else(|| format!("{key} conformance is missing"))?; + require_toml_string(conformance, "vector", VECTOR_RELATIVE, &key)?; + let expected_case_kinds = if expected.id == "publication_artifact.from_canonical_json" { + vec![ + "publication_artifact.from_canonical_json.valid".to_owned(), + "publication_artifact.from_canonical_json.invalid".to_owned(), + ] + } else { + vec![format!("{}.valid", expected.id)] + }; + if toml_string_array(&key, conformance.get("case_kinds"))? != expected_case_kinds { + return Err(format!("{key} conformance case kinds drifted")); + } + } + 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} must declare changes"))?; + let matching = changes + .iter() + .filter(|change| change.get("id").and_then(toml::Value::as_str) == Some(RELEASE_CHANGE_ID)) + .collect::<Vec<_>>(); + let [change] = matching.as_slice() else { + return Err(format!( + "{RELEASE_RELATIVE} must contain {RELEASE_CHANGE_ID} exactly once" + )); + }; + if change.get("classification").and_then(toml::Value::as_str) != Some("feature") + || toml_string_array(RELEASE_CHANGE_ID, change.get("semver_impacts"))? + != [ + "add_exported_type", + "add_exported_function", + "add_exported_constant", + "add_conformance_vector", + ] + || change + .get("summary") + .and_then(toml::Value::as_str) + .is_none_or(str::is_empty) + { + return Err(format!("{RELEASE_CHANGE_ID} release authority drifted")); + } + 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_result_vector(workspace_root: &Path) -> Result<ValidatedResultVector, 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}"))?; + if suite.suite != "phase1_publication_artifact" || suite.contract_version != "1.0.0" { + return Err(format!("{VECTOR_RELATIVE} identity drifted")); + } + let mut ids = BTreeSet::new(); + let mut valid = Vec::new(); + let mut invalid = Vec::new(); + for case in suite.vectors { + if !ids.insert(case.id.clone()) { + return Err(format!("{VECTOR_RELATIVE} duplicates case {}", case.id)); + } + if !case.input.is_object() || !case.expected.is_object() { + return Err(format!("vector case {} must use object data", case.id)); + } + if case.kind == INVALID_VECTOR_KIND { + invalid.push(case.id); + } else if VALID_VECTOR_CASES + .iter() + .any(|(id, kind)| *id == case.id && *kind == case.kind) + { + valid.push((case.id, case.kind)); + } else { + return Err(format!( + "vector case {} uses unknown or mismatched kind {}", + case.id, case.kind + )); + } + } + let expected_valid = VALID_VECTOR_CASES + .iter() + .map(|(id, kind)| ((*id).to_owned(), (*kind).to_owned())) + .collect::<Vec<_>>(); + if valid != expected_valid { + return Err(format!( + "{VECTOR_RELATIVE} valid inventory drifted: expected {expected_valid:?}, found {valid:?}" + )); + } + for required in REQUIRED_INVALID_VECTOR_IDS { + if invalid + .iter() + .filter(|value| value.as_str() == *required) + .count() + != 1 + { + return Err(format!( + "{VECTOR_RELATIVE} must contain invalid case {required} exactly once" + )); + } + } + Ok(ValidatedResultVector { + valid_case_ids: valid.into_iter().map(|(id, _)| id).collect(), + invalid_case_ids: invalid, + bytes, + }) +} + +fn validate_immutable_raw_predecessor_under_lock(workspace_root: &Path) -> Result<(), String> { + for spec in RAW_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 raw-source rebuild predecessor artifact {} drifted", + spec.relative + )); + } + } + let manifest_bytes = read_regular_file(workspace_root, RAW_MANIFEST_RELATIVE)?; + let manifest: Value = serde_json::from_slice(&manifest_bytes) + .map_err(|error| format!("parse {RAW_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_store.raw_source_rebuild_v1") + { + return Err(format!("{RAW_MANIFEST_RELATIVE} identity drifted")); + } + let sidecar = read_regular_file(workspace_root, RAW_MANIFEST_SHA256_RELATIVE)?; + if sidecar != format!("{}\n", sha256_hex(&manifest_bytes)).as_bytes() { + return Err(format!( + "{RAW_MANIFEST_SHA256_RELATIVE} does not authenticate its manifest" + )); + } + + let superseded = RAW_PREDECESSOR_SUPERSEDED_PATHS + .iter() + .copied() + .collect::<BTreeSet<_>>(); + if superseded.len() != RAW_PREDECESSOR_SUPERSEDED_PATHS.len() { + return Err("raw predecessor supersession paths must be unique".to_owned()); + } + let sources = manifest + .get("source_files") + .and_then(Value::as_array) + .ok_or_else(|| format!("{RAW_MANIFEST_RELATIVE} has no source_files array"))?; + let mut seen = BTreeSet::new(); + for source in sources { + let path = descriptor_path(source, "raw predecessor source")?; + if !seen.insert(path) { + return Err(format!("{RAW_MANIFEST_RELATIVE} duplicates source {path}")); + } + if !superseded.contains(path) { + validate_value_descriptor(workspace_root, source, "raw predecessor source")?; + } + } + for path in superseded { + if !seen.contains(path) { + return Err(format!( + "raw predecessor supersession path {path} is not predecessor-bound" + )); + } + } + for descriptor in manifest + .get("migration_inventory") + .and_then(Value::as_array) + .ok_or_else(|| format!("{RAW_MANIFEST_RELATIVE} has no migration inventory"))? + { + validate_value_descriptor(workspace_root, descriptor, "raw migration")?; + } + validate_value_descriptor( + workspace_root, + manifest + .get("manifest_schema") + .ok_or_else(|| format!("{RAW_MANIFEST_RELATIVE} has no manifest_schema"))?, + "raw manifest schema", + )?; + validate_value_descriptor( + workspace_root, + manifest + .pointer("/predecessor/manifest") + .ok_or_else(|| format!("{RAW_MANIFEST_RELATIVE} has no predecessor manifest"))?, + "raw predecessor manifest", + )?; + let executor = json!({ + "path": RAW_VECTOR_EXECUTOR_RELATIVE, + "byte_length": manifest.pointer("/result_vector/executor_byte_length"), + "sha256": manifest.pointer("/result_vector/executor_sha256"), + "hash_algorithm": manifest.pointer("/result_vector/executor_hash_algorithm"), + }); + validate_value_descriptor(workspace_root, &executor, "raw vector executor")?; + if read_regular_file(workspace_root, RAW_VECTOR_RELATIVE)? + != read_regular_file(workspace_root, RAW_VECTOR_MIRROR_RELATIVE)? + { + return Err("immutable raw-source rebuild vector mirror drifted".to_owned()); + } + Ok(()) +} + +fn validate_value_descriptor( + workspace_root: &Path, + descriptor: &Value, + label: &str, +) -> Result<(), String> { + let path = descriptor_path(descriptor, label)?; + let expected_len = descriptor + .get("byte_length") + .and_then(Value::as_u64) + .ok_or_else(|| format!("{label} {path} has no byte_length"))?; + let expected_sha = descriptor + .get("sha256") + .and_then(Value::as_str) + .ok_or_else(|| format!("{label} {path} has no sha256"))?; + if descriptor.get("hash_algorithm").and_then(Value::as_str) != Some(HASH_ALGORITHM) { + return Err(format!("{label} {path} has an unsupported hash algorithm")); + } + let bytes = read_regular_file(workspace_root, path)?; + if bytes.len() as u64 != expected_len || sha256_hex(&bytes) != expected_sha { + return Err(format!( + "{label} {path} drifted from its predecessor descriptor" + )); + } + Ok(()) +} + +fn descriptor_path<'a>(descriptor: &'a Value, label: &str) -> Result<&'a str, String> { + descriptor + .get("path") + .and_then(Value::as_str) + .ok_or_else(|| format!("{label} has no path")) +} + +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)? { + if !path.ends_with(".rs") { + return Err(format!("{root} contains non-Rust production source {path}")); + } + specs.push(("public_production_source".to_owned(), path)); + } + } + specs.extend( + EXPLICIT_SOURCE_SPECS + .iter() + .map(|(role, path)| ((*role).to_owned(), (*path).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 source inventory duplicates {path}")); + } + } + Ok(specs) +} + +fn regular_file_inventory(workspace_root: &Path, relative: &str) -> Result<Vec<String>, String> { + let mut pending = vec![workspace_root.join(relative)]; + let mut files = Vec::new(); + while let Some(directory) = pending.pop() { + let mut entries = fs::read_dir(&directory) + .map_err(|error| format!("read {}: {error}", directory.display()))? + .collect::<Result<Vec<_>, _>>() + .map_err(|error| format!("read {} entry: {error}", directory.display()))?; + entries.sort_by_key(|entry| entry.file_name()); + for entry in entries { + let file_type = entry + .file_type() + .map_err(|error| format!("inspect {}: {error}", entry.path().display()))?; + if file_type.is_symlink() { + return Err(format!( + "governed source inventory forbids symlink {}", + entry.path().display() + )); + } + if file_type.is_dir() { + pending.push(entry.path()); + } else if file_type.is_file() { + files.push( + entry + .path() + .strip_prefix(workspace_root) + .map_err(|error| format!("relativize {}: {error}", entry.path().display()))? + .to_string_lossy() + .replace('\\', "/"), + ); + } else { + return Err(format!( + "governed inventory requires regular files: {}", + entry.path().display() + )); + } + } + } + files.sort(); + Ok(files) +} + +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: byte_length(path, &bytes)?, + 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: byte_length(path, bytes)?, + sha256: sha256_hex(bytes), + hash_algorithm: HASH_ALGORITHM.to_owned(), + }) +} + +fn byte_length(path: &str, bytes: &[u8]) -> Result<u64, String> { + u64::try_from(bytes.len()).map_err(|_| format!("{path} byte length exceeds u64")) +} + +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 require_toml_string( + table: &toml::map::Map<String, toml::Value>, + field: &str, + expected: &str, + label: &str, +) -> Result<(), String> { + if table.get(field).and_then(toml::Value::as_str) != Some(expected) { + return Err(format!("{label}.{field} must be {expected}")); + } + Ok(()) +} + +fn validate_manifest_shape(manifest: &PublicationManifest) -> Result<(), String> { + if manifest.schema_version != SCHEMA_VERSION + || manifest.contract_id != CONTRACT_ID + || manifest.authority_id != AUTHORITY_ID + || manifest.artifact != expected_artifact_descriptor() + || manifest.operations != expected_operation_descriptors() + || manifest.predecessor_source_supersessions + != RAW_PREDECESSOR_SUPERSEDED_PATHS + .iter() + .map(|value| (*value).to_owned()) + .collect::<Vec<_>>() + || manifest.release.change_id != RELEASE_CHANGE_ID + || manifest.result_vector.canonical_path != VECTOR_RELATIVE + || manifest.result_vector.mirror_path != VECTOR_MIRROR_RELATIVE + { + return Err(format!("{MANIFEST_RELATIVE} shape 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 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 sha256_hex(bytes: &[u8]) -> String { + hex::encode(Sha256::digest(bytes)) +} + +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 manifest_schema() -> Value { + json!({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://radroots.org/contracts/event-codec/phase1-publication-artifact-v1.schema.json", + "title": "Radroots Phase 1 Publication Artifact Contract", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "contract_id", + "authority_id", + "manifest_schema", + "predecessor", + "event_contract_registry", + "artifact", + "operations", + "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", "manifest"], + "properties": { + "contract_id": {"const": "radroots_event_store.raw_source_rebuild_v1"}, + "manifest": {"$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": {"type": "string", "minLength": 1} + } + }, + "artifact": { + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "validator", + "semantic_roles", + "serialized_semantic_variants", + "event_subvariants", + "canonical_encoding", + "artifact_max_bytes", + "signed_event_wire_max_bytes", + "media_reference_max_count", + "envelope_fields", + "draft_fields", + "media_reference_fields", + "media_reference_identity", + "primary_media_url_requirement", + "post_fallback_url_requirement", + "digest_algorithm", + "digest_domain", + "digest_domain_terminator", + "digest_preimage", + "constructors", + "denied_inputs", + "reload_capability", + "threat_boundary" + ], + "properties": { + "schema_version": {"const": 1}, + "validator": {"const": "validate_phase1_publication_artifact"}, + "semantic_roles": {"const": ["profile", "update", "photo_update", "ask", "event", "food_availability"]}, + "serialized_semantic_variants": {"const": ["profile", "update", "photo_update", "ask", "event_date", "event_time", "food_availability"]}, + "event_subvariants": {"const": ["date", "time"]}, + "canonical_encoding": {"const": "serde_json_compact_struct_field_order_v1"}, + "artifact_max_bytes": {"const": 2097152}, + "signed_event_wire_max_bytes": {"const": 262144}, + "media_reference_max_count": {"const": 4096}, + "envelope_fields": {"const": ["schema_version", "semantic_variant", "authored_operation_id", "event_contract_id", "expected_author", "draft", "expected_event_id", "media_references", "artifact_digest"]}, + "draft_fields": {"const": ["created_at", "kind", "tags", "content"]}, + "media_reference_fields": {"const": ["url", "sha256", "size", "media_type"]}, + "media_reference_identity": {"const": "exact_case_preserving_approved_url_with_descriptor_commitment_v1"}, + "primary_media_url_requirement": {"const": "blossom_hash_path_extension_required_v1"}, + "post_fallback_url_requirement": {"const": "approved_blossom_url_extension_optional_v1"}, + "digest_algorithm": {"const": "sha256_domain_nul_canonical_json_v1"}, + "digest_domain": {"const": "radroots.phase1.publication-artifact.v1"}, + "digest_domain_terminator": {"const": "0x00"}, + "digest_preimage": {"const": "ascii_domain_then_single_nul_then_canonical_envelope_without_digest_v1"}, + "constructors": { + "type": "array", + "minItems": 7, + "maxItems": 7, + "items": {"$ref": "#/$defs/constructor"} + }, + "denied_inputs": {"const": ["arbitrary_event_draft", "raw_json", "numeric_kind", "signed_event", "private_key", "signer"]}, + "reload_capability": {"const": "persisted_artifact_only_no_prior_capability_restoration_v1"}, + "threat_boundary": {"const": ["detects_accidental_corruption", "detects_payload_only_modification", "detects_digest_only_modification", "does_not_authenticate_actor_rewriting_payload_and_digest", "does_not_survive_validator_binary_or_host_compromise", "does_not_restore_byte_verification_or_upload_completion", "does_not_replace_nip01_id_and_signature_verification"]} + } + }, + "operations": { + "type": "array", + "minItems": 9, + "maxItems": 9, + "items": {"$ref": "#/$defs/operation"} + }, + "predecessor_source_supersessions": { + "type": "array", + "minItems": 6, + "maxItems": 6, + "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": 7, "items": {"type": "string"}}, + "invalid_case_ids": {"type": "array", "minItems": 24, "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} + } + }, + "constructor": { + "type": "object", + "additionalProperties": false, + "required": [ + "semantic_variant", + "serialized_semantic_variant", + "event_variant", + "strict_input", + "constructor", + "publication_operation_id", + "authored_operation_id", + "event_contract_id", + "kind" + ], + "properties": { + "semantic_variant": {"type": "string", "minLength": 1}, + "serialized_semantic_variant": {"type": "string", "minLength": 1}, + "event_variant": {"type": ["string", "null"]}, + "strict_input": {"type": "string", "minLength": 1}, + "constructor": {"type": "string", "minLength": 1}, + "publication_operation_id": {"type": "string", "minLength": 1}, + "authored_operation_id": {"type": "string", "minLength": 1}, + "event_contract_id": {"type": "string", "minLength": 1}, + "kind": {"type": "integer", "minimum": 0} + } + }, + "operation": { + "type": "object", + "additionalProperties": false, + "required": ["id", "strict_input", "output", "error_class", "signing", "transport"], + "properties": { + "id": {"type": "string", "minLength": 1}, + "strict_input": {"type": "string", "minLength": 1}, + "output": {"enum": ["RadrootsPhase1PublicationArtifact", "Bytes"]}, + "error_class": {"enum": ["none", "validation_error", "parse_error"]}, + "signing": {"const": "none"}, + "transport": {"const": "none"} + } + } + } + }) +} + +#[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 publication_source_inventory_is_closed_and_unique() { + let root = workspace_root(); + let specs = source_specs(&root).expect("publication source inventory"); + 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.rs")); + assert!(paths.contains("tools/xtask/src/contract/phase1_publication_artifact.rs")); + for generated in GENERATED_ARTIFACT_PATHS { + assert!(!paths.contains(generated)); + } + } + + #[test] + fn publication_compiler_operations_and_vector_authority_are_current() { + let root = workspace_root(); + validate_compiler_authority(&root).expect("compiler authority"); + validate_operations_authority(&root).expect("operations authority"); + validate_result_vector(&root).expect("result vector"); + } + + #[test] + fn publication_manifest_schema_rejects_unknown_top_level_fields() { + let root = workspace_root(); + let schema_bytes = canonical_json_bytes(&manifest_schema()).expect("schema"); + let manifest = describe_manifest(&root, &schema_bytes).expect("manifest"); + let mut value = serde_json::to_value(manifest).expect("manifest value"); + validate_json_schema(&manifest_schema(), &value).expect("valid manifest"); + value["unexpected"] = Value::Bool(true); + validate_json_schema(&manifest_schema(), &value).expect_err("unknown field must fail"); + } +} diff --git a/tools/xtask/src/contract/raw_source_rebuild.rs b/tools/xtask/src/contract/raw_source_rebuild.rs @@ -7271,17 +7271,14 @@ mod tests { #[test] fn generated_bundle_render_is_deterministic() { let root = workspace_root(); - let first = expected_artifacts(&root) - .expect("first render") - .into_iter() - .map(|artifact| (artifact.relative, artifact.contents)) - .collect::<Vec<_>>(); - let second = expected_artifacts(&root) - .expect("second render") - .into_iter() - .map(|artifact| (artifact.relative, artifact.contents)) - .collect::<Vec<_>>(); - assert_eq!(first, second); + crate::contract::phase1_publication_artifact::validate_immutable_raw_source_rebuild_predecessor( + &root, + ) + .expect("first immutable predecessor validation"); + crate::contract::phase1_publication_artifact::validate_immutable_raw_source_rebuild_predecessor( + &root, + ) + .expect("second immutable predecessor validation"); } #[test] @@ -7489,13 +7486,15 @@ mod tests { #[test] fn schema_rejects_unknown_runtime_fields() { - let schema = manifest_schema(); let root = workspace_root(); - let schema_bytes = canonical_json_bytes(&schema).expect("schema bytes"); - let mut manifest = serde_json::to_value( - describe_manifest(&root, &schema_bytes).expect("current manifest"), + let schema: Value = serde_json::from_slice( + &read_regular_file(&root, MANIFEST_SCHEMA_RELATIVE).expect("immutable schema bytes"), + ) + .expect("immutable schema"); + let mut manifest: Value = serde_json::from_slice( + &read_regular_file(&root, MANIFEST_RELATIVE).expect("immutable manifest bytes"), ) - .expect("manifest value"); + .expect("immutable manifest"); manifest .pointer_mut("/runtime") .and_then(Value::as_object_mut) diff --git a/tools/xtask/src/main.rs b/tools/xtask/src/main.rs @@ -22,6 +22,7 @@ fn usage() { eprintln!(" cargo xtask contract food-availability-projection-manifest [--write]"); 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 knowledge-manifest [--write]"); eprintln!(" cargo xtask dto-roots --check|--write"); eprintln!(" cargo xtask release preflight"); @@ -138,6 +139,16 @@ fn run_contract(args: &[String]) -> Result<(), String> { "raw-source-rebuild-manifest accepts no arguments or exactly --write".to_string(), ), }, + Some("phase1-publication-artifact-manifest") => match &args[1..] { + [] => contract::validate_phase1_publication_artifact_manifest(&workspace_root()), + [flag] if flag == "--write" => { + contract::write_phase1_publication_artifact_manifest(&workspace_root()) + } + _ => Err( + "phase1-publication-artifact-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()) @@ -264,6 +275,12 @@ mod tests { ]) .expect_err("invalid raw-source rebuild manifest mode"); assert!(invalid_raw_source_rebuild.contains("exactly --write")); + let invalid_publication = run_contract(&[ + "phase1-publication-artifact-manifest".to_string(), + "--invalid".to_string(), + ]) + .expect_err("invalid Phase 1 publication manifest mode"); + assert!(invalid_publication.contains("exactly --write")); let unknown_root = run(&["unknown".to_string()]).expect_err("unknown command"); assert!(unknown_root.contains("unknown command")); @@ -365,6 +382,8 @@ mod tests { .expect("contract SourceMaintenance manifest"); run_contract(&["raw-source-rebuild-manifest".to_string()]) .expect("contract raw-source rebuild manifest"); + run_contract(&["phase1-publication-artifact-manifest".to_string()]) + .expect("contract Phase 1 publication artifact manifest"); run_contract(&["knowledge-manifest".to_string()]).expect("contract knowledge manifest"); } }