commit 52019253cdbb3bdd62c1bbc9c24e7be377526b80
parent 077b087a1fbecb00b29c43823279f7af718315c0
Author: triesap <tyson@radroots.org>
Date: Wed, 22 Jul 2026 01:08:52 +0000
event-codec: add typed 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
- reject noncanonical reloads with bounded executable conformance vectors
- add operations, release metadata, and successor authority without changing registry v7
Diffstat:
18 files changed, 7340 insertions(+), 20 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
@@ -171,6 +171,16 @@ 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. Exact canonical JSON binds
+ the operation and event-contract profile, author, frozen unsigned draft,
+ expected NIP-01 id, and a full-URL media commitment inventory under a
+ domain-separated digest. Strict bounded reload rejects unknown 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,262 @@
+{
+ "suite": "phase1_publication_artifact",
+ "contract_version": "1.0.0",
+ "vectors": [
+ {
+ "id": "profile_round_trip",
+ "kind": "publication_artifact.round_trip.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": "10db30ccd1ec4986a43f9ac2fefe201f582e8965ac0c36f0fe467b066194fa21",
+ "canonical_json_bytes": 1305,
+ "canonical_json_sha256": "9ddaee58d68cce2a400880c62a2c3f4b3b11765a2136b3c930f474ec28680b43"
+ }
+ },
+ {
+ "id": "update_round_trip",
+ "kind": "publication_artifact.round_trip.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": "345676a88a68663b16ab22f44117589f7a429aae0fbd6bd5f168e5fc7b2df8ab",
+ "canonical_json_bytes": 525,
+ "canonical_json_sha256": "69d0b7aaec8e2fb3d85a677ef9ac1b6d3eb56d05b9ba7b98737687bfba074110"
+ }
+ },
+ {
+ "id": "photo_update_round_trip",
+ "kind": "publication_artifact.round_trip.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": "e9b83a57a78adaf2740ec0fd819c8754d96ae325d5a6f785be99e0267fa29251",
+ "canonical_json_bytes": 1415,
+ "canonical_json_sha256": "ebb29fa48adda8660f35218d879bcf1535d5584bead441ba10f996b54bcdbc76"
+ }
+ },
+ {
+ "id": "ask_round_trip_with_fallback",
+ "kind": "publication_artifact.round_trip.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": "cfb3c4c74ec56edb5d4bbbed6c08f33cf9202a16c12e3fd41e54839ba8fe1762",
+ "canonical_json_bytes": 1411,
+ "canonical_json_sha256": "b7f3eec4930f40642e69c90ab293d2fc8f370c95286794c33d55cda942a1f696"
+ }
+ },
+ {
+ "id": "event_date_round_trip",
+ "kind": "publication_artifact.round_trip.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": "0a5b30799263dff58fd88e5b581eca13b0eb324dfe2698471459821af847d040",
+ "canonical_json_bytes": 1013,
+ "canonical_json_sha256": "1eba894c4f19b4c4f9dc62e47feb4ce0b1c232b24bc57ca80aa29c007916efd1"
+ }
+ },
+ {
+ "id": "event_time_round_trip",
+ "kind": "publication_artifact.round_trip.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": "a0cdbfdbc6a7067ee65ce6564c5660435074bf85c42baaa52694a03833dcada9",
+ "canonical_json_bytes": 1013,
+ "canonical_json_sha256": "512561b622c5c86e8d5e28045778f0eb9abf8f9eab89cb7ada000d42c6ffd61a"
+ }
+ },
+ {
+ "id": "food_availability_round_trip",
+ "kind": "publication_artifact.round_trip.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": "ff04d736df4b1dd01b17197ef0e9c88678133057130623377954332a9ce3e6b3",
+ "canonical_json_bytes": 1132,
+ "canonical_json_sha256": "b7d55bf0c75acdae53f670cd6a263c036fe619c73601eac5a03c6ef58d91695f"
+ }
+ },
+ {
+ "id": "leading_whitespace_is_noncanonical",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "leading_whitespace" },
+ "expected": { "error": "publication_artifact_non_canonical_json" }
+ },
+ {
+ "id": "unknown_field_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "unknown_field" },
+ "expected": { "error": "publication_artifact_invalid_json" }
+ },
+ {
+ "id": "unknown_draft_field_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "unknown_draft_field" },
+ "expected": { "error": "publication_artifact_invalid_json" }
+ },
+ {
+ "id": "unknown_media_field_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "unknown_media_field" },
+ "expected": { "error": "publication_artifact_invalid_json" }
+ },
+ {
+ "id": "json_field_order_is_noncanonical",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "json_field_order" },
+ "expected": { "error": "publication_artifact_non_canonical_json" }
+ },
+ {
+ "id": "unknown_version_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "schema_version" },
+ "expected": { "error": "publication_artifact_version_unsupported" }
+ },
+ {
+ "id": "cross_variant_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "all_cross_variants" },
+ "expected": { "error": "publication_authored_operation_mismatch" }
+ },
+ {
+ "id": "operation_mismatch_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "operation_id" },
+ "expected": { "error": "publication_authored_operation_mismatch" }
+ },
+ {
+ "id": "contract_mismatch_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "contract_id" },
+ "expected": { "error": "publication_event_contract_mismatch" }
+ },
+ {
+ "id": "kind_mismatch_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "kind" },
+ "expected": { "error": "publication_kind_mismatch" }
+ },
+ {
+ "id": "author_mismatch_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "author" },
+ "expected": { "error": "publication_expected_author_invalid" }
+ },
+ {
+ "id": "created_at_tamper_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "created_at" },
+ "expected": { "error": "publication_expected_event_id_mismatch" }
+ },
+ {
+ "id": "draft_tags_tamper_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "draft_tags" },
+ "expected": { "error": "publication_expected_event_id_mismatch" }
+ },
+ {
+ "id": "draft_content_tamper_is_rejected",
+ "kind": "publication_artifact.reload.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.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "noncanonical_nip05" },
+ "expected": { "error": "publication_profile_invalid" }
+ },
+ {
+ "id": "empty_ask_content_is_rejected_after_id_rebuild",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "ask", "mutation": "empty_ask_content" },
+ "expected": { "error": "publication_post_profile_invalid" }
+ },
+ {
+ "id": "expected_event_id_tamper_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "expected_event_id" },
+ "expected": { "error": "publication_expected_event_id_mismatch" }
+ },
+ {
+ "id": "digest_tamper_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "digest" },
+ "expected": { "error": "publication_artifact_digest_mismatch" }
+ },
+ {
+ "id": "media_order_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "media_order" },
+ "expected": { "error": "publication_media_inventory_non_canonical" }
+ },
+ {
+ "id": "profile_media_commitment_tamper_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "media_size" },
+ "expected": { "error": "publication_artifact_digest_mismatch" }
+ },
+ {
+ "id": "media_url_tamper_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "media_url" },
+ "expected": { "error": "publication_media_inventory_mismatch" }
+ },
+ {
+ "id": "media_hash_tamper_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "media_hash" },
+ "expected": { "error": "publication_media_reference_invalid" }
+ },
+ {
+ "id": "media_type_tamper_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "media_type" },
+ "expected": { "error": "publication_artifact_digest_mismatch" }
+ },
+ {
+ "id": "post_media_commitment_must_match_imeta",
+ "kind": "publication_artifact.reload.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",
@@ -654,6 +662,206 @@ 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/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"
+
+[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/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"
+
+[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/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"
+
+[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/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"
+
+[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/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"
+
+[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/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"
+
+[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/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"
+
+[operations.phase1_publication_artifact_reload]
+domain = "publication"
+id = "publication_artifact.reload"
+stability = "beta"
+inputs = ["Bytes"]
+outputs = ["RadrootsPhase1PublicationArtifact"]
+error_class = "parse_error"
+deterministic = true
+signing = "none"
+transport = "none"
+
+[operations.phase1_publication_artifact_reload.implementation]
+rust_modules = ["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_reload.conformance]
+vector = "contracts/conformance/vectors/publication/phase1_artifact.v1.json"
+
[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 an authored-only, canonical, tamper-evident persistence artifact for the closed Phase 1 Profile, root Post, typed Event, and FoodAvailability publication set."
+
+[[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,12 @@
# 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 and emits bounded canonical
+JSON suitable for persistence. Reloading validates the exact profile, NIP-01
+identifier, media inventory, and domain-separated digest, 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,2340 @@
+{
+ "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": 9568,
+ "sha256": "c8d25afc65de8c21a83decefa62036385cb826d58248cc070cd1948ffcab1ec4",
+ "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": "b8737a9c5836517114e7df6c2194c46e3c200093e12c4e6297165d2b9dae56a1",
+ "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,
+ "semantic_variants": [
+ "profile",
+ "update",
+ "photo_update",
+ "ask",
+ "event",
+ "food_availability"
+ ],
+ "event_subvariants": [
+ "date",
+ "time"
+ ],
+ "canonical_encoding": "serde_json_compact_struct_field_order_v1",
+ "artifact_max_bytes": 524288,
+ "media_reference_max_count": 4096,
+ "digest_algorithm": "sha256_domain_nul_canonical_json_v1",
+ "digest_domain": "radroots.phase1.publication-artifact.v1\u0000",
+ "constructors": [
+ {
+ "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",
+ "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",
+ "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",
+ "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",
+ "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",
+ "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",
+ "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
+ }
+ ],
+ "persisted_fields": [
+ "schema_version",
+ "semantic_variant",
+ "authored_operation_id",
+ "event_contract_id",
+ "expected_author",
+ "draft.created_at",
+ "draft.kind",
+ "draft.tags",
+ "draft.content",
+ "draft.expected_event_id",
+ "media_references[].url",
+ "media_references[].sha256",
+ "media_references[].size",
+ "media_references[].media_type",
+ "artifact_digest"
+ ],
+ "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.reload",
+ "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": 29665,
+ "sha256": "9433606926a70efc33e89658d9a1cf807d1fe51c31d1135f5f3d4dd188546e5b",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "workspace_lockfile_authority",
+ "path": "Cargo.lock",
+ "byte_length": 216965,
+ "sha256": "f26bf62f77e48914c89c15e689fdbc6799928e9603cab38f50c0c65a0c405edf",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "workspace_manifest_authority",
+ "path": "Cargo.toml",
+ "byte_length": 10836,
+ "sha256": "285532dbb0894204843a832880f136ceac5ee312a3203ff951fb0551fac63ec4",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "operations_authority",
+ "path": "contracts/operations.toml",
+ "byte_length": 73838,
+ "sha256": "45faa0dc3f71bc6b75c59e6746b25a2689e33cf09d6c6bbbaf8311defc9ef2d4",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "release_authority",
+ "path": "contracts/releases/1.0.0-alpha.1.toml",
+ "byte_length": 20216,
+ "sha256": "c7b5a9878c71720027f97357c11cfdd624acac9ccf7a6d6011c7a12d186708aa",
+ "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": 20441,
+ "sha256": "b8a88edb2a82aaf06a33b7cdedb48ea9c5d66eee52e961bf5cbc1e709f0969f6",
+ "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": 2540,
+ "sha256": "872deae9f6b43b2f3f4c48794fce47f4e635f88dbc6df054bb26306976ea4745",
+ "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": 52207,
+ "sha256": "7edf56bf53680f14f0a684cf7ab34a85a4a4d367d858ab0dafdb7285a444d860",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "publication_vector_executor",
+ "path": "crates/event_codec/tests/publication_artifact.rs",
+ "byte_length": 24811,
+ "sha256": "1762d456f0c2958011fd66edf6e0bc8c19c7ebb5ace45a157fca3de1cfb606cd",
+ "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": 1097,
+ "sha256": "7e858f4f33913f986c565be2a31c41615ea0585c9e19572363ef5cae36cafdc9",
+ "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": 852224,
+ "sha256": "22cf608eb6dae5d25f54ff439f01bf9dad8943998b455e198fff3e53383ba176",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "publication_contract_governance",
+ "path": "tools/xtask/src/contract/phase1_publication_artifact.rs",
+ "byte_length": 62736,
+ "sha256": "a52c8c6de697789176fbd1cc2e4b48795397f38b5bf1a43029520f5399f4d707",
+ "hash_algorithm": "sha256_bytes_v1"
+ },
+ {
+ "role": "superseded_raw_rebuild_contract_governance",
+ "path": "tools/xtask/src/contract/raw_source_rebuild.rs",
+ "byte_length": 294574,
+ "sha256": "e9b7b50fbe4e1d5890137dd634f7ddcece2fdd9aaf68904d4c4e3510d4ff5d05",
+ "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": 11213,
+ "sha256": "dadf61674ae27672c22f924a9ff0636fce2d5eab6c37f8305c1c702e5a899ee8",
+ "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"
+ ],
+ "invalid_case_ids": [
+ "leading_whitespace_is_noncanonical",
+ "unknown_field_is_rejected",
+ "unknown_draft_field_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",
+ "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,421 @@
+{
+ "$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"
+ },
+ "strict_input": {
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "required": [
+ "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": [
+ "validation_error",
+ "parse_error"
+ ]
+ },
+ "id": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "output": {
+ "const": "RadrootsPhase1PublicationArtifact"
+ },
+ "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": {
+ "minimum": 1,
+ "type": "integer"
+ },
+ "canonical_encoding": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "constructors": {
+ "items": {
+ "$ref": "#/$defs/constructor"
+ },
+ "maxItems": 7,
+ "minItems": 7,
+ "type": "array"
+ },
+ "denied_inputs": {
+ "items": {
+ "type": "string"
+ },
+ "minItems": 1,
+ "type": "array"
+ },
+ "digest_algorithm": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "digest_domain": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "event_subvariants": {
+ "items": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "maxItems": 2,
+ "minItems": 2,
+ "type": "array"
+ },
+ "media_reference_max_count": {
+ "minimum": 1,
+ "type": "integer"
+ },
+ "persisted_fields": {
+ "items": {
+ "type": "string"
+ },
+ "minItems": 1,
+ "type": "array"
+ },
+ "reload_capability": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "schema_version": {
+ "const": 1
+ },
+ "semantic_variants": {
+ "items": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "maxItems": 6,
+ "minItems": 6,
+ "type": "array"
+ },
+ "threat_boundary": {
+ "items": {
+ "type": "string"
+ },
+ "minItems": 1,
+ "type": "array"
+ }
+ },
+ "required": [
+ "schema_version",
+ "semantic_variants",
+ "event_subvariants",
+ "canonical_encoding",
+ "artifact_max_bytes",
+ "media_reference_max_count",
+ "digest_algorithm",
+ "digest_domain",
+ "constructors",
+ "persisted_fields",
+ "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": 8,
+ "minItems": 8,
+ "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 @@
+d5182e42a61627c26535cdf7e337365d104e0a02b8202f2c973ef8a82e018d28
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,1380 @@
+//! 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, 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 = 512 * 1024;
+pub const RADROOTS_PHASE1_PUBLICATION_MEDIA_MAX_COUNT: usize = 4096;
+
+const ARTIFACT_DIGEST_DOMAIN: &[u8] = b"radroots.phase1.publication-artifact.v1\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,
+ expected_event_id: RadrootsEventId,
+}
+
+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
+ }
+
+ pub fn expected_event_id(&self) -> &RadrootsEventId {
+ &self.expected_event_id
+ }
+}
+
+/// 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,
+ 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 media_references(&self) -> &[RadrootsPhase1PublicationMediaReference] {
+ &self.media_references
+ }
+
+ pub const fn artifact_digest(&self) -> RadrootsPhase1PublicationArtifactDigest {
+ self.artifact_digest
+ }
+
+ pub fn canonical_json(&self) -> &[u8] {
+ &self.canonical_json
+ }
+
+ 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 = RadrootsEventId::parse(&wire.draft.expected_event_id)
+ .map_err(|_| RadrootsPhase1PublicationArtifactError::InvalidExpectedEventId)?;
+ let draft = RadrootsPhase1PublicationDraft {
+ created_at: wire.draft.created_at,
+ kind: wire.draft.kind,
+ tags: wire.draft.tags,
+ content: wire.draft.content,
+ expected_event_id,
+ };
+ validate_draft_identifier(&expected_author, &draft)?;
+
+ 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, &media_references)?;
+ if computed != artifact_digest {
+ return Err(RadrootsPhase1PublicationArtifactError::DigestMismatch);
+ }
+ let canonical_json = serialize_artifact(
+ variant,
+ &expected_author,
+ &draft,
+ &media_references,
+ artifact_digest,
+ )?;
+ if canonical_json != bytes {
+ return Err(RadrootsPhase1PublicationArtifactError::NonCanonicalJson);
+ }
+ Ok(Self {
+ semantic_variant: variant,
+ expected_author,
+ draft,
+ 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,
+ expected_event_id,
+ };
+ validate_phase1_publication_profile(variant, &draft, &media_references)?;
+ let artifact_digest =
+ compute_artifact_digest(variant, &expected_author, &draft, &media_references)?;
+ let canonical_json = serialize_artifact(
+ variant,
+ &expected_author,
+ &draft,
+ &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,
+ 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 reloaded =
+ RadrootsPhase1PublicationArtifact::from_canonical_json(artifact.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 },
+ 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::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::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 validate_draft_identifier(
+ author: &RadrootsPublicKey,
+ draft: &RadrootsPhase1PublicationDraft,
+) -> 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 != draft.expected_event_id {
+ return Err(RadrootsPhase1PublicationArtifactError::ExpectedEventIdMismatch);
+ }
+ Ok(())
+}
+
+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);
+ }
+ validate_media_urls(
+ content
+ .picture
+ .iter()
+ .chain(content.banner.iter())
+ .map(String::as_str),
+ 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)?;
+ 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);
+ }
+ 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(),
+ ]);
+ 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_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,
+ expected_event_id: 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,
+ 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,
+ media_references: Vec<MediaReferenceWire>,
+}
+
+fn artifact_payload_wire<'a>(
+ variant: RadrootsPhase1PublicationSemanticVariant,
+ author: &'a RadrootsPublicKey,
+ draft: &RadrootsPhase1PublicationDraft,
+ 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: draft.expected_event_id.as_str().to_string(),
+ },
+ media_references: media
+ .iter()
+ .map(RadrootsPhase1PublicationMediaReference::to_wire)
+ .collect(),
+ }
+}
+
+fn compute_artifact_digest(
+ variant: RadrootsPhase1PublicationSemanticVariant,
+ author: &RadrootsPublicKey,
+ draft: &RadrootsPhase1PublicationDraft,
+ media: &[RadrootsPhase1PublicationMediaReference],
+) -> Result<RadrootsPhase1PublicationArtifactDigest, RadrootsPhase1PublicationArtifactError> {
+ let payload = serde_json::to_vec(&artifact_payload_wire(variant, author, draft, media))
+ .map_err(|_| RadrootsPhase1PublicationArtifactError::Serialization)?;
+ let mut hasher = Sha256::new();
+ hasher.update(ARTIFACT_DIGEST_DOMAIN);
+ hasher.update(payload);
+ Ok(RadrootsPhase1PublicationArtifactDigest(
+ hasher.finalize().into(),
+ ))
+}
+
+fn serialize_artifact(
+ variant: RadrootsPhase1PublicationSemanticVariant,
+ author: &RadrootsPublicKey,
+ draft: &RadrootsPhase1PublicationDraft,
+ media: &[RadrootsPhase1PublicationMediaReference],
+ digest: RadrootsPhase1PublicationArtifactDigest,
+) -> Result<Vec<u8>, RadrootsPhase1PublicationArtifactError> {
+ let payload = artifact_payload_wire(variant, author, draft, 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,
+ media_references: payload.media_references,
+ artifact_digest: digest.to_hex(),
+ })
+ .map_err(|_| RadrootsPhase1PublicationArtifactError::Serialization)
+}
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,262 @@
+{
+ "suite": "phase1_publication_artifact",
+ "contract_version": "1.0.0",
+ "vectors": [
+ {
+ "id": "profile_round_trip",
+ "kind": "publication_artifact.round_trip.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": "10db30ccd1ec4986a43f9ac2fefe201f582e8965ac0c36f0fe467b066194fa21",
+ "canonical_json_bytes": 1305,
+ "canonical_json_sha256": "9ddaee58d68cce2a400880c62a2c3f4b3b11765a2136b3c930f474ec28680b43"
+ }
+ },
+ {
+ "id": "update_round_trip",
+ "kind": "publication_artifact.round_trip.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": "345676a88a68663b16ab22f44117589f7a429aae0fbd6bd5f168e5fc7b2df8ab",
+ "canonical_json_bytes": 525,
+ "canonical_json_sha256": "69d0b7aaec8e2fb3d85a677ef9ac1b6d3eb56d05b9ba7b98737687bfba074110"
+ }
+ },
+ {
+ "id": "photo_update_round_trip",
+ "kind": "publication_artifact.round_trip.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": "e9b83a57a78adaf2740ec0fd819c8754d96ae325d5a6f785be99e0267fa29251",
+ "canonical_json_bytes": 1415,
+ "canonical_json_sha256": "ebb29fa48adda8660f35218d879bcf1535d5584bead441ba10f996b54bcdbc76"
+ }
+ },
+ {
+ "id": "ask_round_trip_with_fallback",
+ "kind": "publication_artifact.round_trip.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": "cfb3c4c74ec56edb5d4bbbed6c08f33cf9202a16c12e3fd41e54839ba8fe1762",
+ "canonical_json_bytes": 1411,
+ "canonical_json_sha256": "b7f3eec4930f40642e69c90ab293d2fc8f370c95286794c33d55cda942a1f696"
+ }
+ },
+ {
+ "id": "event_date_round_trip",
+ "kind": "publication_artifact.round_trip.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": "0a5b30799263dff58fd88e5b581eca13b0eb324dfe2698471459821af847d040",
+ "canonical_json_bytes": 1013,
+ "canonical_json_sha256": "1eba894c4f19b4c4f9dc62e47feb4ce0b1c232b24bc57ca80aa29c007916efd1"
+ }
+ },
+ {
+ "id": "event_time_round_trip",
+ "kind": "publication_artifact.round_trip.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": "a0cdbfdbc6a7067ee65ce6564c5660435074bf85c42baaa52694a03833dcada9",
+ "canonical_json_bytes": 1013,
+ "canonical_json_sha256": "512561b622c5c86e8d5e28045778f0eb9abf8f9eab89cb7ada000d42c6ffd61a"
+ }
+ },
+ {
+ "id": "food_availability_round_trip",
+ "kind": "publication_artifact.round_trip.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": "ff04d736df4b1dd01b17197ef0e9c88678133057130623377954332a9ce3e6b3",
+ "canonical_json_bytes": 1132,
+ "canonical_json_sha256": "b7d55bf0c75acdae53f670cd6a263c036fe619c73601eac5a03c6ef58d91695f"
+ }
+ },
+ {
+ "id": "leading_whitespace_is_noncanonical",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "leading_whitespace" },
+ "expected": { "error": "publication_artifact_non_canonical_json" }
+ },
+ {
+ "id": "unknown_field_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "unknown_field" },
+ "expected": { "error": "publication_artifact_invalid_json" }
+ },
+ {
+ "id": "unknown_draft_field_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "unknown_draft_field" },
+ "expected": { "error": "publication_artifact_invalid_json" }
+ },
+ {
+ "id": "unknown_media_field_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "unknown_media_field" },
+ "expected": { "error": "publication_artifact_invalid_json" }
+ },
+ {
+ "id": "json_field_order_is_noncanonical",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "json_field_order" },
+ "expected": { "error": "publication_artifact_non_canonical_json" }
+ },
+ {
+ "id": "unknown_version_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "schema_version" },
+ "expected": { "error": "publication_artifact_version_unsupported" }
+ },
+ {
+ "id": "cross_variant_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "all_cross_variants" },
+ "expected": { "error": "publication_authored_operation_mismatch" }
+ },
+ {
+ "id": "operation_mismatch_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "operation_id" },
+ "expected": { "error": "publication_authored_operation_mismatch" }
+ },
+ {
+ "id": "contract_mismatch_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "contract_id" },
+ "expected": { "error": "publication_event_contract_mismatch" }
+ },
+ {
+ "id": "kind_mismatch_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "kind" },
+ "expected": { "error": "publication_kind_mismatch" }
+ },
+ {
+ "id": "author_mismatch_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "author" },
+ "expected": { "error": "publication_expected_author_invalid" }
+ },
+ {
+ "id": "created_at_tamper_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "created_at" },
+ "expected": { "error": "publication_expected_event_id_mismatch" }
+ },
+ {
+ "id": "draft_tags_tamper_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "draft_tags" },
+ "expected": { "error": "publication_expected_event_id_mismatch" }
+ },
+ {
+ "id": "draft_content_tamper_is_rejected",
+ "kind": "publication_artifact.reload.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.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "noncanonical_nip05" },
+ "expected": { "error": "publication_profile_invalid" }
+ },
+ {
+ "id": "empty_ask_content_is_rejected_after_id_rebuild",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "ask", "mutation": "empty_ask_content" },
+ "expected": { "error": "publication_post_profile_invalid" }
+ },
+ {
+ "id": "expected_event_id_tamper_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "expected_event_id" },
+ "expected": { "error": "publication_expected_event_id_mismatch" }
+ },
+ {
+ "id": "digest_tamper_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "digest" },
+ "expected": { "error": "publication_artifact_digest_mismatch" }
+ },
+ {
+ "id": "media_order_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "media_order" },
+ "expected": { "error": "publication_media_inventory_non_canonical" }
+ },
+ {
+ "id": "profile_media_commitment_tamper_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "media_size" },
+ "expected": { "error": "publication_artifact_digest_mismatch" }
+ },
+ {
+ "id": "media_url_tamper_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "media_url" },
+ "expected": { "error": "publication_media_inventory_mismatch" }
+ },
+ {
+ "id": "media_hash_tamper_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "media_hash" },
+ "expected": { "error": "publication_media_reference_invalid" }
+ },
+ {
+ "id": "media_type_tamper_is_rejected",
+ "kind": "publication_artifact.reload.invalid",
+ "input": { "fixture": "profile", "mutation": "media_type" },
+ "expected": { "error": "publication_artifact_digest_mismatch" }
+ },
+ {
+ "id": "post_media_commitment_must_match_imeta",
+ "kind": "publication_artifact.reload.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,680 @@
+#![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, 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>,
+ 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(), 31);
+ let artifacts = all_artifacts();
+
+ for case in suite.vectors {
+ let artifact = artifact_fixture(&artifacts, &case.input.fixture);
+ match case.kind.as_str() {
+ "publication_artifact.round_trip.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.draft().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.canonical_json().len(),
+ case.expected.canonical_json_bytes.unwrap(),
+ "{}",
+ case.id
+ );
+ assert_eq!(
+ RadrootsBlossomSha256::digest(artifact.canonical_json()).to_hex(),
+ case.expected.canonical_json_sha256.unwrap(),
+ "{}",
+ case.id
+ );
+ assert_eq!(
+ RadrootsPhase1PublicationArtifact::from_canonical_json(
+ artifact.canonical_json()
+ )
+ .unwrap(),
+ *artifact,
+ "{}",
+ case.id
+ );
+ }
+ "publication_artifact.reload.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.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);
+ assert!(artifact.canonical_json().len() <= RADROOTS_PHASE1_PUBLICATION_ARTIFACT_MAX_BYTES);
+ let reloaded =
+ RadrootsPhase1PublicationArtifact::from_canonical_json(artifact.canonical_json())
+ .unwrap();
+ assert_eq!(&reloaded, artifact);
+ assert_eq!(reloaded.to_canonical_json(), artifact.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.canonical_json()).unwrap(),
+ artifact
+ );
+}
+
+#[test]
+fn publication_artifact_reload_rejects_cross_variant_and_every_envelope_tamper() {
+ let artifact = &all_artifacts()[0];
+ let canonical = artifact.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["draft"]["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_reload_rejects_media_order_and_commitment_tamper() {
+ let artifact = &all_artifacts()[0];
+ let mut value: Value = serde_json::from_slice(artifact.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.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.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 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(),
+ }
+ );
+}
+
+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.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> {
+ if mutation == "leading_whitespace" {
+ let mut bytes = vec![b' '];
+ bytes.extend_from_slice(canonical);
+ return bytes;
+ }
+ 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),
+ "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["draft"]["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_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 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["draft"]["expected_event_id"] = Value::from(
+ compute_canonical_nip01_event_id(author, created_at, kind, &tags, content)
+ .unwrap()
+ .to_string(),
+ );
+}
+
+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
@@ -17323,6 +17323,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,1668 @@
+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,
+};
+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_IDS: &[&str] = &[
+ "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",
+];
+
+const REQUIRED_INVALID_VECTOR_IDS: &[&str] = &[
+ "leading_whitespace_is_noncanonical",
+ "unknown_field_is_rejected",
+ "unknown_draft_field_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",
+ "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,
+ "b8737a9c5836517114e7df6c2194c46e3c200093e12c4e6297165d2b9dae56a1",
+ ),
+ ImmutableArtifactSpec::new(
+ RAW_MANIFEST_SCHEMA_RELATIVE,
+ 17_896,
+ "f9d210967e54b66f39c8bb965d97b2001a0ebc0927e7c2c14edb8e474bfda695",
+ ),
+ ImmutableArtifactSpec::new(
+ RAW_MANIFEST_SHA256_RELATIVE,
+ 65,
+ "737ee2e4ecd400e1c647e80422c432cd2955d7c7cc04fdf3f9993551480e7957",
+ ),
+ ImmutableArtifactSpec::new(
+ RAW_GENERATED_DESCRIPTOR_RELATIVE,
+ 50_735,
+ "20ad0d83304bb4ea3aeb0b37fc068891f1f2e5a0c3abc93d1a9932770330307c",
+ ),
+ 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",
+ 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",
+ 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",
+ 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",
+ 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",
+ 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",
+ 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",
+ 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,
+ 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,
+ semantic_variants: Vec<String>,
+ event_subvariants: Vec<String>,
+ canonical_encoding: String,
+ artifact_max_bytes: u64,
+ media_reference_max_count: u64,
+ digest_algorithm: String,
+ digest_domain: String,
+ constructors: Vec<ConstructorDescriptor>,
+ persisted_fields: Vec<String>,
+ 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,
+ 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,
+ semantic_variants: [
+ "profile",
+ "update",
+ "photo_update",
+ "ask",
+ "event",
+ "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,
+ media_reference_max_count: RADROOTS_PHASE1_PUBLICATION_MEDIA_MAX_COUNT as u64,
+ digest_algorithm: "sha256_domain_nul_canonical_json_v1".to_owned(),
+ digest_domain: "radroots.phase1.publication-artifact.v1\0".to_owned(),
+ constructors: CONSTRUCTORS
+ .iter()
+ .map(|spec| ConstructorDescriptor {
+ semantic_variant: spec.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(),
+ persisted_fields: [
+ "schema_version",
+ "semantic_variant",
+ "authored_operation_id",
+ "event_contract_id",
+ "expected_author",
+ "draft.created_at",
+ "draft.kind",
+ "draft.tags",
+ "draft.content",
+ "draft.expected_event_id",
+ "media_references[].url",
+ "media_references[].sha256",
+ "media_references[].size",
+ "media_references[].media_type",
+ "artifact_digest",
+ ]
+ .into_iter()
+ .map(str::to_owned)
+ .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.reload".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 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())?;
+ if operations.contains_key("phase1_publication_artifact_build") {
+ return Err("ambiguous aggregate publication build operation is forbidden".to_owned());
+ }
+ for (index, expected) in expected_operation_descriptors().iter().enumerate() {
+ let key = if index < CONSTRUCTORS.len() {
+ format!(
+ "phase1_publication_artifact_build_{}",
+ expected
+ .id
+ .strip_prefix("publication_artifact.build_")
+ .expect("constructor operation prefix")
+ )
+ } else {
+ "phase1_publication_artifact_reload".to_owned()
+ };
+ let operation = operations
+ .get(&key)
+ .and_then(toml::Value::as_table)
+ .ok_or_else(|| format!("operations.toml is missing {key}"))?;
+ let expected_inputs = if index < CONSTRUCTORS.len() {
+ vec![
+ expected.strict_input.clone(),
+ "u64".to_owned(),
+ "String".to_owned(),
+ ]
+ } else {
+ vec!["Bytes".to_owned()]
+ };
+ 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"))?
+ != ["RadrootsPhase1PublicationArtifact"]
+ {
+ 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)?;
+ }
+ 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));
+ }
+ match case.kind.as_str() {
+ "publication_artifact.round_trip.valid" => valid.push(case.id),
+ "publication_artifact.reload.invalid" => invalid.push(case.id),
+ other => return Err(format!("vector case uses unknown kind {other}")),
+ }
+ }
+ if valid != VALID_VECTOR_IDS {
+ return Err(format!(
+ "{VECTOR_RELATIVE} valid inventory drifted: expected {VALID_VECTOR_IDS:?}, 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,
+ 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",
+ "semantic_variants",
+ "event_subvariants",
+ "canonical_encoding",
+ "artifact_max_bytes",
+ "media_reference_max_count",
+ "digest_algorithm",
+ "digest_domain",
+ "constructors",
+ "persisted_fields",
+ "denied_inputs",
+ "reload_capability",
+ "threat_boundary"
+ ],
+ "properties": {
+ "schema_version": {"const": 1},
+ "semantic_variants": {
+ "type": "array",
+ "minItems": 6,
+ "maxItems": 6,
+ "items": {"type": "string", "minLength": 1}
+ },
+ "event_subvariants": {
+ "type": "array",
+ "minItems": 2,
+ "maxItems": 2,
+ "items": {"type": "string", "minLength": 1}
+ },
+ "canonical_encoding": {"type": "string", "minLength": 1},
+ "artifact_max_bytes": {"type": "integer", "minimum": 1},
+ "media_reference_max_count": {"type": "integer", "minimum": 1},
+ "digest_algorithm": {"type": "string", "minLength": 1},
+ "digest_domain": {"type": "string", "minLength": 1},
+ "constructors": {
+ "type": "array",
+ "minItems": 7,
+ "maxItems": 7,
+ "items": {"$ref": "#/$defs/constructor"}
+ },
+ "persisted_fields": {"type": "array", "minItems": 1, "items": {"type": "string"}},
+ "denied_inputs": {"type": "array", "minItems": 1, "items": {"type": "string"}},
+ "reload_capability": {"type": "string", "minLength": 1},
+ "threat_boundary": {"type": "array", "minItems": 1, "items": {"type": "string"}}
+ }
+ },
+ "operations": {
+ "type": "array",
+ "minItems": 8,
+ "maxItems": 8,
+ "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",
+ "event_variant",
+ "strict_input",
+ "constructor",
+ "publication_operation_id",
+ "authored_operation_id",
+ "event_contract_id",
+ "kind"
+ ],
+ "properties": {
+ "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": {"const": "RadrootsPhase1PublicationArtifact"},
+ "error_class": {"enum": ["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");
}
}