lib

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

lib.rs (105707B)


      1 #![forbid(unsafe_code)]
      2 
      3 use radroots_blossom::{BlobDescriptor, BlobUrl, Error, MediaType, Sha256};
      4 use radroots_event::envelope::{EventEnvelope, EventEnvelopeError, EventEnvelopeParts};
      5 use radroots_event::farm::Farm;
      6 use radroots_event::farm::coop::Coop;
      7 use radroots_event::farm::crdt::FarmCrdtChange;
      8 use radroots_event::farm::file::FarmFileMetadata;
      9 use radroots_event::farm::plot::Plot;
     10 use radroots_event::farm::workspace::FarmWorkspaceManifest;
     11 use radroots_event::knowledge::{
     12     KnowledgeClaim, KnowledgeFieldReport, KnowledgeRelation, KnowledgeReview, KnowledgeSource,
     13     WikiArticle, WikiMergeRequest, WikiRedirect,
     14 };
     15 use radroots_event::listing::operational::OperationalListing;
     16 use radroots_event::media::file_metadata::FileMetadata;
     17 use radroots_event::media::{AuthoredImage, AuthoredImageError};
     18 use radroots_event::post::article::Article;
     19 use radroots_event::post::comment::{
     20     AuthoredNip22Comment, Nip22AddressRootReference, Nip22CommentError,
     21     Nip22CommentParentReference, Nip22CommentRoot, Nip22EventRootReference,
     22 };
     23 use radroots_event::post::document::Document;
     24 use radroots_event::post::reaction::Reaction;
     25 use radroots_event::post::report::Report;
     26 use radroots_event::post::repost::{GenericRepost, Repost};
     27 use radroots_event::post::{
     28     AuthoredAsk, AuthoredPhotoUpdate, AuthoredPostError, AuthoredPostImage, AuthoredUpdate,
     29     PostImageDimensions,
     30 };
     31 use radroots_event::social::follow::Follow;
     32 use radroots_event::social::gift_wrap::GiftWrap;
     33 use radroots_event::social::group::{
     34     GroupAdmins, GroupCreateGroup, GroupCreateInvite, GroupDeleteEvent, GroupDeleteGroup,
     35     GroupEditMetadata, GroupJoinRequest, GroupLeaveRequest, GroupMembers, GroupMetadata,
     36     GroupPutUser, GroupRemoveUser, GroupRoles,
     37 };
     38 use radroots_event::social::http_auth::HttpAuth;
     39 use radroots_event::social::job_feedback::JobFeedback;
     40 use radroots_event::social::job_request::JobRequest;
     41 use radroots_event::social::job_result::JobResult;
     42 use radroots_event::social::list::List;
     43 use radroots_event::social::list_set::ListSet;
     44 use radroots_event::social::message::Message;
     45 use radroots_event::social::message_file::MessageFile;
     46 use radroots_event::social::relay_auth::RelayAuth;
     47 use radroots_event::social::seal::Seal;
     48 use radroots_event::wire::Nip01EventWireParts;
     49 use radroots_event_codec::encode::article::article_build_tags;
     50 use radroots_event_codec::encode::comment::authored_nip22_comment_to_wire_parts;
     51 use radroots_event_codec::encode::coop::coop_build_tags;
     52 use radroots_event_codec::encode::document::document_build_tags;
     53 use radroots_event_codec::encode::farm::farm_build_tags;
     54 use radroots_event_codec::encode::farm_crdt::farm_crdt_change_build_tags_with_author;
     55 use radroots_event_codec::encode::farm_file::farm_file_metadata_build_tags;
     56 use radroots_event_codec::encode::farm_workspace::farm_workspace_build_tags;
     57 use radroots_event_codec::encode::file_metadata::file_metadata_build_tags;
     58 use radroots_event_codec::encode::follow::follow_build_tags;
     59 use radroots_event_codec::encode::gift_wrap::gift_wrap_build_tags;
     60 use radroots_event_codec::encode::group::{
     61     group_admins_build_tags, group_create_group_build_tags, group_create_invite_build_tags,
     62     group_delete_event_build_tags, group_delete_group_build_tags, group_edit_metadata_build_tags,
     63     group_join_request_build_tags, group_leave_request_build_tags, group_members_build_tags,
     64     group_metadata_build_tags, group_put_user_build_tags, group_remove_user_build_tags,
     65     group_roles_build_tags,
     66 };
     67 use radroots_event_codec::encode::http_auth::http_auth_build_tags;
     68 use radroots_event_codec::encode::job::feedback::job_feedback_build_tags;
     69 use radroots_event_codec::encode::job::request::job_request_build_tags;
     70 use radroots_event_codec::encode::job::result::job_result_build_tags;
     71 use radroots_event_codec::encode::knowledge::{
     72     knowledge_claim_build_tags, knowledge_field_report_build_tags, knowledge_relation_build_tags,
     73     knowledge_review_build_tags, knowledge_source_build_tags, wiki_article_build_tags,
     74     wiki_merge_request_build_tags, wiki_redirect_build_tags,
     75 };
     76 use radroots_event_codec::encode::list::list_build_tags;
     77 use radroots_event_codec::encode::list_set::list_set_build_tags;
     78 use radroots_event_codec::encode::message::message_build_tags;
     79 use radroots_event_codec::encode::message_file::message_file_build_tags;
     80 use radroots_event_codec::encode::operational_listing::{
     81     operational_listing_tags as operational_listing_tags_impl,
     82     operational_listing_tags_full as operational_listing_tags_full_impl,
     83 };
     84 use radroots_event_codec::encode::plot::plot_build_tags;
     85 use radroots_event_codec::encode::post::{
     86     authored_ask_to_wire_parts, authored_photo_update_to_wire_parts, authored_update_to_wire_parts,
     87 };
     88 use radroots_event_codec::encode::reaction::reaction_build_tags;
     89 use radroots_event_codec::encode::relay_auth::relay_auth_build_tags;
     90 use radroots_event_codec::encode::report::report_build_tags;
     91 use radroots_event_codec::encode::repost::{generic_repost_build_tags, repost_build_tags};
     92 use radroots_event_codec::encode::seal::seal_build_tags;
     93 use radroots_event_codec::verify::{RadrootsDecodeError, RadrootsDecodedEvent};
     94 use radroots_event_codec::{
     95     authoring::AuthoredEventPlan,
     96     decode::rhi::{
     97         RadrootsRhiEvidenceAttestationOutcomeV1, RadrootsRhiEvidenceAttestationV1,
     98         rhi_evidence_attestation_from_verified_event,
     99     },
    100     verify::{Nip01SignatureVerifier, id as verify_event_id, signature as verify_event_signature},
    101 };
    102 use radroots_trade::evidence::{
    103     RadrootsRhiEvidenceReportV1, RadrootsTradeEvidenceCoverageV1, RadrootsTradeEvidenceManifestV1,
    104     RadrootsTradeEvidenceOutcomeV1,
    105 };
    106 use serde::de::DeserializeOwned;
    107 use serde::{Deserialize, Serialize};
    108 #[cfg(target_arch = "wasm32")]
    109 use wasm_bindgen::JsValue;
    110 #[cfg(target_arch = "wasm32")]
    111 use wasm_bindgen::prelude::*;
    112 
    113 #[cfg(target_arch = "wasm32")]
    114 type RadrootsJsValue = JsValue;
    115 
    116 #[cfg(not(target_arch = "wasm32"))]
    117 type RadrootsJsValue = String;
    118 
    119 fn err_js<E: ToString>(err: E) -> RadrootsJsValue {
    120     #[cfg(target_arch = "wasm32")]
    121     {
    122         JsValue::from_str(&err.to_string())
    123     }
    124     #[cfg(not(target_arch = "wasm32"))]
    125     {
    126         err.to_string()
    127     }
    128 }
    129 
    130 fn error_json(code: &str, inner_code: Option<&str>) -> RadrootsJsValue {
    131     let value = serde_json::json!({
    132         "code": code,
    133         "inner_code": inner_code,
    134     });
    135     err_js(value)
    136 }
    137 
    138 fn normalized_payload(input: &str) -> &str {
    139     if input.is_empty() { "{}" } else { input }
    140 }
    141 
    142 fn parse_json<T: DeserializeOwned>(input: &str) -> Result<T, RadrootsJsValue> {
    143     serde_json::from_str(normalized_payload(input)).map_err(err_js)
    144 }
    145 
    146 fn parse_authored_json<T: DeserializeOwned>(input: &str) -> Result<T, RadrootsJsValue> {
    147     serde_json::from_str(input).map_err(|_| error_json("invalid_json", Some("authored_input")))
    148 }
    149 
    150 fn authored_error(code: &str) -> RadrootsJsValue {
    151     error_json("encode_error", Some(code))
    152 }
    153 
    154 fn blossom_authored_error(error: Error) -> RadrootsJsValue {
    155     authored_error(error.code())
    156 }
    157 
    158 fn post_authored_error(error: AuthoredPostError) -> RadrootsJsValue {
    159     authored_error(error.code())
    160 }
    161 
    162 fn image_authored_error(error: AuthoredImageError) -> RadrootsJsValue {
    163     authored_error(error.code())
    164 }
    165 
    166 fn comment_authored_error(error: Nip22CommentError) -> RadrootsJsValue {
    167     authored_error(error.code())
    168 }
    169 
    170 fn wire_parts_to_json(parts: &Nip01EventWireParts) -> Result<String, RadrootsJsValue> {
    171     serde_json::to_string(parts)
    172         .map_err(|_| error_json("internal_error", Some("wire_parts_serialization")))
    173 }
    174 
    175 fn tags_to_json(tags: Vec<Vec<String>>) -> Result<String, RadrootsJsValue> {
    176     serde_json::to_string(&tags).map_err(err_js)
    177 }
    178 
    179 #[derive(Serialize)]
    180 struct EvidenceManifestJson {
    181     contract_id: &'static str,
    182     contract_version: u16,
    183     trade_id: String,
    184     trade_generation: String,
    185     observed_at_unix_s: String,
    186     coverage: &'static str,
    187     evidence_policy_digest: String,
    188     manifest_digest: String,
    189     canonical_bytes_hex: String,
    190 }
    191 
    192 #[derive(Serialize)]
    193 struct EvidenceSupersessionJson {
    194     report_id: String,
    195     event_id: String,
    196 }
    197 
    198 #[derive(Serialize)]
    199 struct EvidenceReportJson {
    200     contract_id: &'static str,
    201     contract_version: u16,
    202     issuer_pubkey: String,
    203     trade_id: String,
    204     claim_mutation_id: String,
    205     outcome: &'static str,
    206     reason_codes: Vec<String>,
    207     projection_digest: String,
    208     evidence_manifest_digest: String,
    209     evidence_policy_digest: String,
    210     observed_at_unix_s: String,
    211     trade_generation: String,
    212     statement_digest: String,
    213     supersession: Option<EvidenceSupersessionJson>,
    214     canonical_content: String,
    215 }
    216 
    217 #[derive(Serialize)]
    218 struct TypedEvidenceEventPlanJson {
    219     contract_id: String,
    220     kind: u32,
    221     author_pubkey: String,
    222     created_at_unix_s: String,
    223     expected_event_id: String,
    224     tags: Vec<Vec<String>>,
    225     content: String,
    226 }
    227 
    228 #[derive(Serialize)]
    229 struct EvidenceAttestationJson {
    230     issuer_pubkey: String,
    231     trade_id: String,
    232     claim_mutation_id: String,
    233     outcome: &'static str,
    234     observed_at_unix_s: String,
    235     trade_generation: String,
    236     statement_digest: String,
    237     supersession: Option<EvidenceSupersessionJson>,
    238     canonical_content: String,
    239 }
    240 
    241 fn coverage_name(value: RadrootsTradeEvidenceCoverageV1) -> &'static str {
    242     match value {
    243         RadrootsTradeEvidenceCoverageV1::Missing => "missing",
    244         RadrootsTradeEvidenceCoverageV1::Partial => "partial",
    245         RadrootsTradeEvidenceCoverageV1::ScopeSatisfied => "scope_satisfied",
    246         RadrootsTradeEvidenceCoverageV1::Unsupported => "unsupported",
    247     }
    248 }
    249 
    250 fn outcome_name(value: RadrootsTradeEvidenceOutcomeV1) -> &'static str {
    251     match value {
    252         RadrootsTradeEvidenceOutcomeV1::Valid => "valid",
    253         RadrootsTradeEvidenceOutcomeV1::Invalid => "invalid",
    254         RadrootsTradeEvidenceOutcomeV1::Indeterminate => "indeterminate",
    255     }
    256 }
    257 
    258 fn attestation_outcome_name(value: RadrootsRhiEvidenceAttestationOutcomeV1) -> &'static str {
    259     match value {
    260         RadrootsRhiEvidenceAttestationOutcomeV1::Valid => "valid",
    261         RadrootsRhiEvidenceAttestationOutcomeV1::Invalid => "invalid",
    262         RadrootsRhiEvidenceAttestationOutcomeV1::Indeterminate => "indeterminate",
    263     }
    264 }
    265 
    266 fn evidence_report_json(value: &RadrootsRhiEvidenceReportV1) -> EvidenceReportJson {
    267     EvidenceReportJson {
    268         contract_id: value.contract_id(),
    269         contract_version: value.contract_version(),
    270         issuer_pubkey: value.issuer_public_key().to_hex(),
    271         trade_id: value.trade_id().to_string(),
    272         claim_mutation_id: value.claim_mutation_id().to_string(),
    273         outcome: outcome_name(value.outcome()),
    274         reason_codes: value
    275             .reason_codes()
    276             .iter()
    277             .map(|code| code.as_str().to_owned())
    278             .collect(),
    279         projection_digest: value.projection_digest().to_hex(),
    280         evidence_manifest_digest: value.evidence_manifest_digest().to_hex(),
    281         evidence_policy_digest: value.evidence_policy_digest().to_hex(),
    282         observed_at_unix_s: value.observed_at_unix_s().to_string(),
    283         trade_generation: value.trade_generation().get().to_string(),
    284         statement_digest: value.statement_digest().to_hex(),
    285         supersession: value
    286             .supersession()
    287             .map(|supersession| EvidenceSupersessionJson {
    288                 report_id: supersession.report_id().to_hex(),
    289                 event_id: supersession.event_id().to_hex(),
    290             }),
    291         canonical_content: value.canonical_content().to_owned(),
    292     }
    293 }
    294 
    295 fn typed_plan_json(value: &AuthoredEventPlan) -> TypedEvidenceEventPlanJson {
    296     TypedEvidenceEventPlanJson {
    297         contract_id: value.body().contract().contract_id().as_str().to_owned(),
    298         kind: value.body().kind(),
    299         author_pubkey: value.author().to_hex(),
    300         created_at_unix_s: value.created_at().to_string(),
    301         expected_event_id: value.expected_event_id().to_hex(),
    302         tags: value.body().tags().to_vec(),
    303         content: value.body().content().to_owned(),
    304     }
    305 }
    306 
    307 #[derive(Deserialize)]
    308 #[serde(deny_unknown_fields)]
    309 struct EventEnvelopeInput {
    310     id: String,
    311     author: String,
    312     created_at: u64,
    313     kind: u32,
    314     tags: Vec<Vec<String>>,
    315     content: String,
    316     sig: String,
    317 }
    318 
    319 fn envelope_error_code(error: &EventEnvelopeError) -> &'static str {
    320     match error {
    321         EventEnvelopeError::InvalidId(_) => "id_invalid",
    322         EventEnvelopeError::InvalidAuthor(_) => "author_invalid",
    323         EventEnvelopeError::InvalidSignature(_) => "signature_invalid",
    324         EventEnvelopeError::NonCanonicalId => "id_non_canonical",
    325         EventEnvelopeError::NonCanonicalAuthor => "author_non_canonical",
    326         EventEnvelopeError::NonCanonicalSignature => "signature_non_canonical",
    327         EventEnvelopeError::EmptyTag { .. } => "tag_empty",
    328         EventEnvelopeError::EmptyTagKey { .. } => "tag_key_empty",
    329         EventEnvelopeError::ControlCharacterTagKey { .. } => "tag_key_control_character",
    330         EventEnvelopeError::ContentTooLarge { .. } => "content_too_large",
    331         EventEnvelopeError::TooManyTags { .. } => "too_many_tags",
    332         EventEnvelopeError::TooManyTagElements { .. } => "too_many_tag_elements",
    333         EventEnvelopeError::TagElementTooLarge { .. } => "tag_element_too_large",
    334         EventEnvelopeError::TagsTooLarge { .. } => "tags_too_large",
    335     }
    336 }
    337 
    338 fn parse_event_json(input: &str) -> Result<EventEnvelope, RadrootsJsValue> {
    339     let envelope: EventEnvelopeInput =
    340         serde_json::from_str(input).map_err(|_| error_json("invalid_json", Some("event_json")))?;
    341     EventEnvelope::new(EventEnvelopeParts {
    342         id: envelope.id,
    343         author: envelope.author,
    344         created_at: envelope.created_at,
    345         kind: envelope.kind,
    346         tags: envelope.tags,
    347         content: envelope.content,
    348         sig: envelope.sig,
    349     })
    350     .map_err(|error| error_json("invalid_event", Some(envelope_error_code(&error))))
    351 }
    352 
    353 fn parse_bounded_event_json(input: &str) -> Result<EventEnvelope, RadrootsJsValue> {
    354     if input.len() > radroots_event::wire::v1::DEFAULT_RAW_JSON_MAX_BYTES {
    355         return Err(error_json("invalid_event", Some("event_too_large")));
    356     }
    357     parse_event_json(input)
    358 }
    359 
    360 fn build_tags_json<T, E, F>(input: &str, build: F) -> Result<String, RadrootsJsValue>
    361 where
    362     T: DeserializeOwned,
    363     E: ToString,
    364     F: FnOnce(&T) -> Result<Vec<Vec<String>>, E>,
    365 {
    366     let value = parse_json::<T>(input)?;
    367     let tags = build(&value).map_err(err_js)?;
    368     tags_to_json(tags)
    369 }
    370 
    371 fn build_tags_json_infallible<T, F>(input: &str, build: F) -> Result<String, RadrootsJsValue>
    372 where
    373     T: DeserializeOwned,
    374     F: FnOnce(&T) -> Vec<Vec<String>>,
    375 {
    376     let value = parse_json::<T>(input)?;
    377     let tags = build(&value);
    378     tags_to_json(tags)
    379 }
    380 
    381 #[derive(Serialize)]
    382 struct DecodedEventJson<'a, T>
    383 where
    384     T: Serialize,
    385 {
    386     event_type: &'static str,
    387     contract_id: &'static str,
    388     event: &'a radroots_event::envelope::EventEnvelope,
    389     payload: &'a T,
    390 }
    391 
    392 fn decoded_event_to_json(decoded: RadrootsDecodedEvent) -> Result<String, RadrootsJsValue> {
    393     match decoded {
    394         RadrootsDecodedEvent::WikiArticle(parsed) => {
    395             decoded_payload_to_json("wiki_article", "radroots.wiki.article.v1", &parsed)
    396         }
    397         RadrootsDecodedEvent::WikiRedirect(parsed) => {
    398             decoded_payload_to_json("wiki_redirect", "radroots.wiki.redirect.v1", &parsed)
    399         }
    400         RadrootsDecodedEvent::WikiMergeRequest(parsed) => decoded_payload_to_json(
    401             "wiki_merge_request",
    402             "radroots.wiki.merge_request.v1",
    403             &parsed,
    404         ),
    405         RadrootsDecodedEvent::KnowledgeSource(parsed) => {
    406             decoded_payload_to_json("knowledge_source", "radroots.knowledge.source.v1", &parsed)
    407         }
    408         RadrootsDecodedEvent::KnowledgeClaim(parsed) => {
    409             decoded_payload_to_json("knowledge_claim", "radroots.knowledge.claim.v1", &parsed)
    410         }
    411         RadrootsDecodedEvent::KnowledgeRelation(parsed) => decoded_payload_to_json(
    412             "knowledge_relation",
    413             "radroots.knowledge.relation.v1",
    414             &parsed,
    415         ),
    416         RadrootsDecodedEvent::KnowledgeReview(parsed) => {
    417             decoded_payload_to_json("knowledge_review", "radroots.knowledge.review.v1", &parsed)
    418         }
    419         RadrootsDecodedEvent::KnowledgeFieldReport(parsed) => decoded_payload_to_json(
    420             "knowledge_field_report",
    421             "radroots.knowledge.field_report.v1",
    422             &parsed,
    423         ),
    424         RadrootsDecodedEvent::EvidenceBounty(parsed) => decoded_payload_to_json(
    425             "evidence_bounty",
    426             "radroots.knowledge.evidence_bounty.v1",
    427             &parsed,
    428         ),
    429         RadrootsDecodedEvent::KnowledgeChangeProposal(parsed) => decoded_payload_to_json(
    430             "knowledge_change_proposal",
    431             "radroots.knowledge.change_proposal.v1",
    432             &parsed,
    433         ),
    434         RadrootsDecodedEvent::ContributionAttestation(parsed) => decoded_payload_to_json(
    435             "contribution_attestation",
    436             "radroots.knowledge.contribution_attestation.v1",
    437             &parsed,
    438         ),
    439     }
    440 }
    441 
    442 fn decoded_payload_to_json<T>(
    443     event_type: &'static str,
    444     contract_id: &'static str,
    445     parsed: &radroots_event_codec::decode::RadrootsParsedEvent<T>,
    446 ) -> Result<String, RadrootsJsValue>
    447 where
    448     T: Serialize,
    449 {
    450     serde_json::to_string(&DecodedEventJson {
    451         event_type,
    452         contract_id,
    453         event: &parsed.event,
    454         payload: &parsed.data.data,
    455     })
    456     .map_err(err_js)
    457 }
    458 
    459 fn decode_error_json(error: RadrootsDecodeError) -> RadrootsJsValue {
    460     let inner = match &error {
    461         RadrootsDecodeError::Nip01Verification(error) => Some(error.code()),
    462         RadrootsDecodeError::ContractValidation(error) => Some(error.code()),
    463         RadrootsDecodeError::EventParse(error) => Some(error.code()),
    464         RadrootsDecodeError::UnsupportedContract { .. } => None,
    465     };
    466     error_json(error.code(), inner)
    467 }
    468 
    469 #[derive(serde::Deserialize)]
    470 struct FarmCrdtTagsInput {
    471     change: FarmCrdtChange,
    472     author_pubkey: String,
    473 }
    474 
    475 #[derive(Deserialize)]
    476 #[serde(deny_unknown_fields)]
    477 struct AuthoredUpdateInput {
    478     content: String,
    479 }
    480 
    481 #[derive(Deserialize)]
    482 #[serde(deny_unknown_fields)]
    483 struct AuthoredMediaPostInput {
    484     content: String,
    485     #[serde(default)]
    486     images: Vec<AuthoredImageInput>,
    487 }
    488 
    489 #[derive(Deserialize)]
    490 #[serde(deny_unknown_fields)]
    491 struct AuthoredImageInput {
    492     bytes: Vec<u8>,
    493     url: String,
    494     media_type: String,
    495     uploaded: u64,
    496     width: u32,
    497     height: u32,
    498     alt: String,
    499     #[serde(default)]
    500     fallbacks: Vec<String>,
    501 }
    502 
    503 #[derive(Deserialize)]
    504 #[serde(deny_unknown_fields)]
    505 struct AuthoredCommentInput {
    506     content: String,
    507     root: AuthoredCommentRootInput,
    508     position: AuthoredCommentPositionInput,
    509 }
    510 
    511 #[derive(Deserialize)]
    512 #[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
    513 enum AuthoredCommentRootInput {
    514     Event {
    515         event_id: String,
    516         author: String,
    517         kind: u32,
    518         #[serde(default)]
    519         relay: Option<String>,
    520     },
    521     Address {
    522         coordinate: String,
    523         author: String,
    524         kind: u32,
    525         #[serde(default)]
    526         relay: Option<String>,
    527     },
    528 }
    529 
    530 #[derive(Deserialize)]
    531 #[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
    532 enum AuthoredCommentPositionInput {
    533     TopEvent,
    534     TopAddress { current_revision: String },
    535     Nested { parent: AuthoredCommentParentInput },
    536 }
    537 
    538 #[derive(Deserialize)]
    539 #[serde(deny_unknown_fields)]
    540 struct AuthoredCommentParentInput {
    541     event_id: String,
    542     author: String,
    543     #[serde(default)]
    544     relay: Option<String>,
    545 }
    546 
    547 fn authored_post_images(
    548     inputs: Vec<AuthoredImageInput>,
    549 ) -> Result<Vec<AuthoredPostImage>, RadrootsJsValue> {
    550     inputs.into_iter().map(authored_post_image).collect()
    551 }
    552 
    553 fn authored_post_image(input: AuthoredImageInput) -> Result<AuthoredPostImage, RadrootsJsValue> {
    554     let media_type = MediaType::parse(&input.media_type).map_err(blossom_authored_error)?;
    555     let sha256 = Sha256::digest(&input.bytes);
    556     let size = u64::try_from(input.bytes.len())
    557         .map_err(|_| authored_error("blob_size_platform_overflow"))?;
    558     let descriptor = BlobDescriptor::new(
    559         BlobUrl::parse(&input.url).map_err(blossom_authored_error)?,
    560         sha256,
    561         size,
    562         media_type.clone(),
    563         input.uploaded,
    564     )
    565     .map_err(blossom_authored_error)?
    566     .approve_reference()
    567     .map_err(blossom_authored_error)?
    568     .verify_bytes(&input.bytes, &media_type)
    569     .map_err(blossom_authored_error)?;
    570     let image =
    571         AuthoredImage::try_from_verified_descriptor(descriptor).map_err(image_authored_error)?;
    572     let dimensions =
    573         PostImageDimensions::new(input.width, input.height).map_err(post_authored_error)?;
    574     let mut authored =
    575         AuthoredPostImage::new(image, dimensions, input.alt).map_err(post_authored_error)?;
    576     for fallback in input.fallbacks {
    577         let fallback = BlobUrl::parse(&fallback)
    578             .map_err(blossom_authored_error)?
    579             .approve()
    580             .map_err(blossom_authored_error)?;
    581         authored = authored
    582             .try_with_fallback(fallback)
    583             .map_err(post_authored_error)?;
    584     }
    585     Ok(authored)
    586 }
    587 
    588 fn authored_comment_from_input(
    589     input: AuthoredCommentInput,
    590 ) -> Result<AuthoredNip22Comment, RadrootsJsValue> {
    591     let root = match input.root {
    592         AuthoredCommentRootInput::Event {
    593             event_id,
    594             author,
    595             kind,
    596             relay,
    597         } => Nip22CommentRoot::Event(
    598             Nip22EventRootReference::parse(event_id, author, kind, relay.as_deref())
    599                 .map_err(comment_authored_error)?,
    600         ),
    601         AuthoredCommentRootInput::Address {
    602             coordinate,
    603             author,
    604             kind,
    605             relay,
    606         } => {
    607             let root = Nip22AddressRootReference::parse(coordinate, relay.as_deref())
    608                 .map_err(comment_authored_error)?;
    609             if root.author().to_hex() != author {
    610                 return Err(authored_error("comment_root_author_mismatch"));
    611             }
    612             if root.kind().as_u32() != kind {
    613                 return Err(authored_error("comment_root_kind_mismatch"));
    614             }
    615             Nip22CommentRoot::Address(root)
    616         }
    617     };
    618 
    619     match (root, input.position) {
    620         (Nip22CommentRoot::Event(root), AuthoredCommentPositionInput::TopEvent) => {
    621             AuthoredNip22Comment::top_level_event(input.content, root)
    622                 .map_err(comment_authored_error)
    623         }
    624         (
    625             Nip22CommentRoot::Address(root),
    626             AuthoredCommentPositionInput::TopAddress { current_revision },
    627         ) => AuthoredNip22Comment::parse_top_level_address(input.content, root, current_revision)
    628             .map_err(comment_authored_error),
    629         (root, AuthoredCommentPositionInput::Nested { parent }) => {
    630             let parent = Nip22CommentParentReference::parse(
    631                 parent.event_id,
    632                 parent.author,
    633                 parent.relay.as_deref(),
    634             )
    635             .map_err(comment_authored_error)?;
    636             AuthoredNip22Comment::nested(input.content, root, parent)
    637                 .map_err(comment_authored_error)
    638         }
    639         _ => Err(authored_error("comment_root_position_incompatible")),
    640     }
    641 }
    642 
    643 /// Builds deterministic unsigned kind-1 wire parts for a strict Update.
    644 #[cfg_attr(
    645     target_arch = "wasm32",
    646     wasm_bindgen(js_name = social_update_build_authored_draft)
    647 )]
    648 pub fn social_update_build_authored_draft(input_json: &str) -> Result<String, RadrootsJsValue> {
    649     let input = parse_authored_json::<AuthoredUpdateInput>(input_json)?;
    650     let update = AuthoredUpdate::new(input.content).map_err(post_authored_error)?;
    651     wire_parts_to_json(&authored_update_to_wire_parts(&update))
    652 }
    653 
    654 /// Builds deterministic unsigned kind-1 wire parts for a strict PhotoUpdate.
    655 #[cfg_attr(
    656     target_arch = "wasm32",
    657     wasm_bindgen(js_name = social_photo_update_build_authored_draft)
    658 )]
    659 pub fn social_photo_update_build_authored_draft(
    660     input_json: &str,
    661 ) -> Result<String, RadrootsJsValue> {
    662     let input = parse_authored_json::<AuthoredMediaPostInput>(input_json)?;
    663     let images = authored_post_images(input.images)?;
    664     let photo = AuthoredPhotoUpdate::new(input.content, images).map_err(post_authored_error)?;
    665     wire_parts_to_json(&authored_photo_update_to_wire_parts(&photo))
    666 }
    667 
    668 /// Builds deterministic unsigned kind-1 wire parts for a strict Ask.
    669 #[cfg_attr(
    670     target_arch = "wasm32",
    671     wasm_bindgen(js_name = social_ask_build_authored_draft)
    672 )]
    673 pub fn social_ask_build_authored_draft(input_json: &str) -> Result<String, RadrootsJsValue> {
    674     let input = parse_authored_json::<AuthoredMediaPostInput>(input_json)?;
    675     let images = authored_post_images(input.images)?;
    676     let ask = AuthoredAsk::new(input.content, images).map_err(post_authored_error)?;
    677     wire_parts_to_json(&authored_ask_to_wire_parts(&ask))
    678 }
    679 
    680 /// Builds deterministic unsigned kind-1111 wire parts for a strict NIP-22 Comment.
    681 #[cfg_attr(
    682     target_arch = "wasm32",
    683     wasm_bindgen(js_name = social_comment_build_authored_draft)
    684 )]
    685 pub fn social_comment_build_authored_draft(input_json: &str) -> Result<String, RadrootsJsValue> {
    686     let input = parse_authored_json::<AuthoredCommentInput>(input_json)?;
    687     let comment = authored_comment_from_input(input)?;
    688     wire_parts_to_json(&authored_nip22_comment_to_wire_parts(&comment))
    689 }
    690 
    691 #[cfg_attr(
    692     target_arch = "wasm32",
    693     wasm_bindgen(js_name = operational_listing_tags)
    694 )]
    695 pub fn operational_listing_tags(listing_json: &str) -> Result<String, RadrootsJsValue> {
    696     build_tags_json::<OperationalListing, _, _>(listing_json, operational_listing_tags_impl)
    697 }
    698 
    699 #[cfg_attr(
    700     target_arch = "wasm32",
    701     wasm_bindgen(js_name = operational_listing_tags_full)
    702 )]
    703 pub fn operational_listing_tags_full(listing_json: &str) -> Result<String, RadrootsJsValue> {
    704     build_tags_json::<OperationalListing, _, _>(listing_json, operational_listing_tags_full_impl)
    705 }
    706 
    707 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = article_tags))]
    708 pub fn article_tags(article_json: &str) -> Result<String, RadrootsJsValue> {
    709     build_tags_json::<Article, _, _>(article_json, article_build_tags)
    710 }
    711 
    712 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = wiki_article_tags))]
    713 pub fn wiki_article_tags(article_json: &str) -> Result<String, RadrootsJsValue> {
    714     build_tags_json::<WikiArticle, _, _>(article_json, wiki_article_build_tags)
    715 }
    716 
    717 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = wiki_redirect_tags))]
    718 pub fn wiki_redirect_tags(redirect_json: &str) -> Result<String, RadrootsJsValue> {
    719     build_tags_json::<WikiRedirect, _, _>(redirect_json, wiki_redirect_build_tags)
    720 }
    721 
    722 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = wiki_merge_request_tags))]
    723 pub fn wiki_merge_request_tags(request_json: &str) -> Result<String, RadrootsJsValue> {
    724     build_tags_json::<WikiMergeRequest, _, _>(request_json, wiki_merge_request_build_tags)
    725 }
    726 
    727 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = knowledge_source_tags))]
    728 pub fn knowledge_source_tags(source_json: &str) -> Result<String, RadrootsJsValue> {
    729     build_tags_json::<KnowledgeSource, _, _>(source_json, knowledge_source_build_tags)
    730 }
    731 
    732 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = knowledge_claim_tags))]
    733 pub fn knowledge_claim_tags(claim_json: &str) -> Result<String, RadrootsJsValue> {
    734     build_tags_json::<KnowledgeClaim, _, _>(claim_json, knowledge_claim_build_tags)
    735 }
    736 
    737 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = knowledge_relation_tags))]
    738 pub fn knowledge_relation_tags(relation_json: &str) -> Result<String, RadrootsJsValue> {
    739     build_tags_json::<KnowledgeRelation, _, _>(relation_json, knowledge_relation_build_tags)
    740 }
    741 
    742 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = knowledge_review_tags))]
    743 pub fn knowledge_review_tags(review_json: &str) -> Result<String, RadrootsJsValue> {
    744     build_tags_json::<KnowledgeReview, _, _>(review_json, knowledge_review_build_tags)
    745 }
    746 
    747 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = knowledge_field_report_tags))]
    748 pub fn knowledge_field_report_tags(report_json: &str) -> Result<String, RadrootsJsValue> {
    749     build_tags_json::<KnowledgeFieldReport, _, _>(report_json, knowledge_field_report_build_tags)
    750 }
    751 
    752 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = verify_and_decode_event_json))]
    753 pub fn verify_and_decode_event_json(event_json: &str) -> Result<String, RadrootsJsValue> {
    754     let event = parse_event_json(event_json)?;
    755     let decoded = radroots_event_codec::verify::verify_and_decode_radroots_event(event)
    756         .map_err(decode_error_json)?;
    757     decoded_event_to_json(decoded)
    758 }
    759 
    760 /// Parses one bounded canonical evidence manifest supplied as lowercase hex.
    761 #[cfg_attr(
    762     target_arch = "wasm32",
    763     wasm_bindgen(js_name = trade_evidence_manifest_parse_json)
    764 )]
    765 pub fn trade_evidence_manifest_parse_json(
    766     canonical_bytes_hex: &str,
    767 ) -> Result<String, RadrootsJsValue> {
    768     if canonical_bytes_hex.len()
    769         > radroots_trade::evidence::RADROOTS_TRADE_EVIDENCE_MANIFEST_MAXIMUM_BYTES * 2
    770         || !canonical_bytes_hex.len().is_multiple_of(2)
    771         || !canonical_bytes_hex
    772             .bytes()
    773             .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
    774     {
    775         return Err(error_json("invalid_evidence_manifest", None));
    776     }
    777     let canonical_bytes = hex::decode(canonical_bytes_hex)
    778         .map_err(|_| error_json("invalid_evidence_manifest", None))?;
    779     let manifest = RadrootsTradeEvidenceManifestV1::from_canonical_bytes(&canonical_bytes)
    780         .map_err(|_| error_json("invalid_evidence_manifest", None))?;
    781     serde_json::to_string(&EvidenceManifestJson {
    782         contract_id: manifest.contract_id(),
    783         contract_version: manifest.contract_version(),
    784         trade_id: manifest.trade_id().to_string(),
    785         trade_generation: manifest.trade_generation().get().to_string(),
    786         observed_at_unix_s: manifest.observed_at_unix_s().to_string(),
    787         coverage: coverage_name(manifest.coverage()),
    788         evidence_policy_digest: manifest.evidence_policy_digest().to_hex(),
    789         manifest_digest: manifest.digest().to_hex(),
    790         canonical_bytes_hex: hex::encode(manifest.canonical_bytes()),
    791     })
    792     .map_err(|_| error_json("internal_error", Some("evidence_manifest_serialization")))
    793 }
    794 
    795 /// Parses one bounded canonical RHI evidence report.
    796 #[cfg_attr(
    797     target_arch = "wasm32",
    798     wasm_bindgen(js_name = rhi_evidence_report_parse_json)
    799 )]
    800 pub fn rhi_evidence_report_parse_json(canonical_content: &str) -> Result<String, RadrootsJsValue> {
    801     let report = RadrootsRhiEvidenceReportV1::from_canonical_content(canonical_content.as_bytes())
    802         .map_err(|_| error_json("invalid_evidence_report", None))?;
    803     serde_json::to_string(&evidence_report_json(&report))
    804         .map_err(|_| error_json("internal_error", Some("evidence_report_serialization")))
    805 }
    806 
    807 /// Builds one unsigned typed RHI evidence-attestation plan.
    808 #[cfg_attr(
    809     target_arch = "wasm32",
    810     wasm_bindgen(js_name = rhi_evidence_attestation_build_draft)
    811 )]
    812 pub fn rhi_evidence_attestation_build_draft(
    813     canonical_content: &str,
    814     created_at_unix_s: u64,
    815 ) -> Result<String, RadrootsJsValue> {
    816     let report = RadrootsRhiEvidenceReportV1::from_canonical_content(canonical_content.as_bytes())
    817         .map_err(|_| error_json("invalid_evidence_report", None))?;
    818     let attestation =
    819         RadrootsRhiEvidenceAttestationV1::from_canonical_content(report.canonical_content())
    820             .map_err(|_| error_json("invalid_evidence_attestation", None))?;
    821     let plan = AuthoredEventPlan::from_rhi_evidence_attestation(&attestation, created_at_unix_s)
    822         .map_err(|_| error_json("invalid_evidence_attestation_plan", None))?;
    823     serde_json::to_string(&typed_plan_json(&plan))
    824         .map_err(|_| error_json("internal_error", Some("evidence_plan_serialization")))
    825 }
    826 
    827 /// Verifies NIP-01 identity/signature before validating the RHI contract.
    828 #[cfg_attr(
    829     target_arch = "wasm32",
    830     wasm_bindgen(js_name = rhi_evidence_attestation_validate_signed_json)
    831 )]
    832 pub fn rhi_evidence_attestation_validate_signed_json(
    833     event_json: &str,
    834 ) -> Result<String, RadrootsJsValue> {
    835     let event = parse_bounded_event_json(event_json)?;
    836     let verified = verify_event_id(radroots_event::admission::RawEvent::new(event))
    837         .and_then(|event| verify_event_signature(event, &Nip01SignatureVerifier))
    838         .map_err(|_| error_json("invalid_event_signature", None))?;
    839     let attestation = rhi_evidence_attestation_from_verified_event(&verified)
    840         .map_err(|error| error_json("invalid_evidence_attestation", Some(error.code())))?;
    841     let supersession = attestation
    842         .supersession()
    843         .map(|supersession| EvidenceSupersessionJson {
    844             report_id: hex::encode(supersession.report_id()),
    845             event_id: supersession.event_id().to_hex(),
    846         });
    847     serde_json::to_string(&EvidenceAttestationJson {
    848         issuer_pubkey: attestation.issuer().to_hex(),
    849         trade_id: attestation.trade_id().to_string(),
    850         claim_mutation_id: attestation.claim_mutation_id().to_string(),
    851         outcome: attestation_outcome_name(attestation.outcome()),
    852         observed_at_unix_s: attestation.observed_at_unix_s().to_string(),
    853         trade_generation: attestation.trade_generation().get().to_string(),
    854         statement_digest: hex::encode(attestation.statement_digest()),
    855         supersession,
    856         canonical_content: attestation.canonical_content().to_owned(),
    857     })
    858     .map_err(|_| error_json("internal_error", Some("evidence_attestation_serialization")))
    859 }
    860 
    861 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = contract_manifest_json))]
    862 pub fn contract_manifest_json() -> Result<String, RadrootsJsValue> {
    863     radroots_event_codec::manifest::contract_manifest_json().map_err(err_js)
    864 }
    865 
    866 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = file_metadata_tags))]
    867 pub fn file_metadata_tags(metadata_json: &str) -> Result<String, RadrootsJsValue> {
    868     build_tags_json::<FileMetadata, _, _>(metadata_json, file_metadata_build_tags)
    869 }
    870 
    871 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = repost_tags))]
    872 pub fn repost_tags(repost_json: &str) -> Result<String, RadrootsJsValue> {
    873     build_tags_json::<Repost, _, _>(repost_json, repost_build_tags)
    874 }
    875 
    876 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = generic_repost_tags))]
    877 pub fn generic_repost_tags(repost_json: &str) -> Result<String, RadrootsJsValue> {
    878     build_tags_json::<GenericRepost, _, _>(repost_json, generic_repost_build_tags)
    879 }
    880 
    881 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = report_tags))]
    882 pub fn report_tags(report_json: &str) -> Result<String, RadrootsJsValue> {
    883     build_tags_json::<Report, _, _>(report_json, report_build_tags)
    884 }
    885 
    886 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = follow_tags))]
    887 pub fn follow_tags(follow_json: &str) -> Result<String, RadrootsJsValue> {
    888     build_tags_json::<Follow, _, _>(follow_json, follow_build_tags)
    889 }
    890 
    891 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = document_tags))]
    892 pub fn document_tags(document_json: &str) -> Result<String, RadrootsJsValue> {
    893     build_tags_json::<Document, _, _>(document_json, document_build_tags)
    894 }
    895 
    896 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = coop_tags))]
    897 pub fn coop_tags(coop_json: &str) -> Result<String, RadrootsJsValue> {
    898     build_tags_json::<Coop, _, _>(coop_json, coop_build_tags)
    899 }
    900 
    901 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = farm_tags))]
    902 pub fn farm_tags(farm_json: &str) -> Result<String, RadrootsJsValue> {
    903     build_tags_json::<Farm, _, _>(farm_json, farm_build_tags)
    904 }
    905 
    906 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = list_tags))]
    907 pub fn list_tags(list_json: &str) -> Result<String, RadrootsJsValue> {
    908     build_tags_json::<List, _, _>(list_json, list_build_tags)
    909 }
    910 
    911 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = list_set_tags))]
    912 pub fn list_set_tags(list_json: &str) -> Result<String, RadrootsJsValue> {
    913     build_tags_json::<ListSet, _, _>(list_json, list_set_build_tags)
    914 }
    915 
    916 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = plot_tags))]
    917 pub fn plot_tags(plot_json: &str) -> Result<String, RadrootsJsValue> {
    918     build_tags_json::<Plot, _, _>(plot_json, plot_build_tags)
    919 }
    920 
    921 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = job_request_tags))]
    922 pub fn job_request_tags(job_json: &str) -> Result<String, RadrootsJsValue> {
    923     build_tags_json_infallible::<JobRequest, _>(job_json, job_request_build_tags)
    924 }
    925 
    926 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = job_result_tags))]
    927 pub fn job_result_tags(job_json: &str) -> Result<String, RadrootsJsValue> {
    928     build_tags_json_infallible::<JobResult, _>(job_json, job_result_build_tags)
    929 }
    930 
    931 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = job_feedback_tags))]
    932 pub fn job_feedback_tags(job_json: &str) -> Result<String, RadrootsJsValue> {
    933     build_tags_json_infallible::<JobFeedback, _>(job_json, job_feedback_build_tags)
    934 }
    935 
    936 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = reaction_tags))]
    937 pub fn reaction_tags(reaction_json: &str) -> Result<String, RadrootsJsValue> {
    938     build_tags_json::<Reaction, _, _>(reaction_json, reaction_build_tags)
    939 }
    940 
    941 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = message_tags))]
    942 pub fn message_tags(message_json: &str) -> Result<String, RadrootsJsValue> {
    943     build_tags_json::<Message, _, _>(message_json, message_build_tags)
    944 }
    945 
    946 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = message_file_tags))]
    947 pub fn message_file_tags(message_json: &str) -> Result<String, RadrootsJsValue> {
    948     build_tags_json::<MessageFile, _, _>(message_json, message_file_build_tags)
    949 }
    950 
    951 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = seal_tags))]
    952 pub fn seal_tags(seal_json: &str) -> Result<String, RadrootsJsValue> {
    953     build_tags_json::<Seal, _, _>(seal_json, seal_build_tags)
    954 }
    955 
    956 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = gift_wrap_tags))]
    957 pub fn gift_wrap_tags(gift_wrap_json: &str) -> Result<String, RadrootsJsValue> {
    958     build_tags_json::<GiftWrap, _, _>(gift_wrap_json, gift_wrap_build_tags)
    959 }
    960 
    961 #[cfg_attr(
    962     target_arch = "wasm32",
    963     wasm_bindgen(js_name = farm_workspace_manifest_tags)
    964 )]
    965 pub fn farm_workspace_manifest_tags(workspace_json: &str) -> Result<String, RadrootsJsValue> {
    966     build_tags_json::<FarmWorkspaceManifest, _, _>(workspace_json, farm_workspace_build_tags)
    967 }
    968 
    969 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = farm_crdt_change_tags))]
    970 pub fn farm_crdt_change_tags(input_json: &str) -> Result<String, RadrootsJsValue> {
    971     let input = parse_json::<FarmCrdtTagsInput>(input_json)?;
    972     let tags = farm_crdt_change_build_tags_with_author(&input.change, Some(&input.author_pubkey))
    973         .map_err(err_js)?;
    974     tags_to_json(tags)
    975 }
    976 
    977 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = farm_file_metadata_tags))]
    978 pub fn farm_file_metadata_tags(file_json: &str) -> Result<String, RadrootsJsValue> {
    979     build_tags_json::<FarmFileMetadata, _, _>(file_json, farm_file_metadata_build_tags)
    980 }
    981 
    982 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = relay_auth_tags))]
    983 pub fn relay_auth_tags(auth_json: &str) -> Result<String, RadrootsJsValue> {
    984     build_tags_json::<RelayAuth, _, _>(auth_json, relay_auth_build_tags)
    985 }
    986 
    987 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = http_auth_tags))]
    988 pub fn http_auth_tags(auth_json: &str) -> Result<String, RadrootsJsValue> {
    989     build_tags_json::<HttpAuth, _, _>(auth_json, http_auth_build_tags)
    990 }
    991 
    992 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_put_user_tags))]
    993 pub fn group_put_user_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
    994     build_tags_json::<GroupPutUser, _, _>(group_json, group_put_user_build_tags)
    995 }
    996 
    997 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_remove_user_tags))]
    998 pub fn group_remove_user_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
    999     build_tags_json::<GroupRemoveUser, _, _>(group_json, group_remove_user_build_tags)
   1000 }
   1001 
   1002 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_create_group_tags))]
   1003 pub fn group_create_group_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
   1004     build_tags_json::<GroupCreateGroup, _, _>(group_json, group_create_group_build_tags)
   1005 }
   1006 
   1007 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_edit_metadata_tags))]
   1008 pub fn group_edit_metadata_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
   1009     build_tags_json::<GroupEditMetadata, _, _>(group_json, group_edit_metadata_build_tags)
   1010 }
   1011 
   1012 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_delete_group_tags))]
   1013 pub fn group_delete_group_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
   1014     build_tags_json::<GroupDeleteGroup, _, _>(group_json, group_delete_group_build_tags)
   1015 }
   1016 
   1017 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_delete_event_tags))]
   1018 pub fn group_delete_event_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
   1019     build_tags_json::<GroupDeleteEvent, _, _>(group_json, group_delete_event_build_tags)
   1020 }
   1021 
   1022 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_create_invite_tags))]
   1023 pub fn group_create_invite_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
   1024     build_tags_json::<GroupCreateInvite, _, _>(group_json, group_create_invite_build_tags)
   1025 }
   1026 
   1027 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_join_request_tags))]
   1028 pub fn group_join_request_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
   1029     build_tags_json::<GroupJoinRequest, _, _>(group_json, group_join_request_build_tags)
   1030 }
   1031 
   1032 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_leave_request_tags))]
   1033 pub fn group_leave_request_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
   1034     build_tags_json::<GroupLeaveRequest, _, _>(group_json, group_leave_request_build_tags)
   1035 }
   1036 
   1037 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_metadata_tags))]
   1038 pub fn group_metadata_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
   1039     build_tags_json::<GroupMetadata, _, _>(group_json, group_metadata_build_tags)
   1040 }
   1041 
   1042 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_admins_tags))]
   1043 pub fn group_admins_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
   1044     build_tags_json::<GroupAdmins, _, _>(group_json, group_admins_build_tags)
   1045 }
   1046 
   1047 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_members_tags))]
   1048 pub fn group_members_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
   1049     build_tags_json::<GroupMembers, _, _>(group_json, group_members_build_tags)
   1050 }
   1051 
   1052 #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = group_roles_tags))]
   1053 pub fn group_roles_tags(group_json: &str) -> Result<String, RadrootsJsValue> {
   1054     build_tags_json::<GroupRoles, _, _>(group_json, group_roles_build_tags)
   1055 }
   1056 
   1057 #[cfg(test)]
   1058 mod tests {
   1059     use super::*;
   1060     use core::num::NonZeroU64;
   1061     use radroots_core::{Currency, Decimal, Money, Quantity, QuantityPrice, Unit};
   1062     use radroots_event::envelope::kind::{
   1063         KIND_FARM_FILE_METADATA, KIND_FILE_METADATA, KIND_KNOWLEDGE_CLAIM, KIND_KNOWLEDGE_SOURCE,
   1064         KIND_WIKI_ARTICLE,
   1065     };
   1066     use radroots_event::envelope::{EventEnvelope, EventEnvelopeLimits, EventEnvelopeParts};
   1067     use radroots_event::farm::FarmRef;
   1068     use radroots_event::farm::crdt::{
   1069         CrdtBackend, FarmCrdtDocumentKind, FarmSemanticKind, RADROOTS_FARM_CRDT_CHANGE_SCHEMA,
   1070     };
   1071     use radroots_event::farm::file::{FarmFileDimensions, FarmFileMetadata, FarmFileSource};
   1072     use radroots_event::farm::workspace::{
   1073         FarmWorkspaceManifest, FarmWorkspaceMediaServer, FarmWorkspaceRef, FarmWorkspaceRelay,
   1074         FarmWorkspaceRelayMode, RADROOTS_FARM_WORKSPACE_PROTOCOL_VERSION,
   1075         RADROOTS_FARM_WORKSPACE_SCHEMA,
   1076     };
   1077     use radroots_event::id::TradeId;
   1078     use radroots_event::knowledge::{
   1079         AddressableRef, KnowledgeCitationSpan, KnowledgeFieldContext, KnowledgeLocation,
   1080         KnowledgeLocationPrecision, KnowledgeNodeRef, KnowledgeObservation,
   1081         KnowledgeObservationValue, KnowledgeReviewScope, KnowledgeReviewScore,
   1082         KnowledgeReviewTarget, RADROOTS_KNOWLEDGE_CLAIM_SCHEMA,
   1083         RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA, RADROOTS_KNOWLEDGE_RELATION_SCHEMA,
   1084         RADROOTS_KNOWLEDGE_REVIEW_SCHEMA, RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
   1085         RADROOTS_KNOWLEDGE_SOURCE_SCHEMA, WikiArticleVersionRef,
   1086     };
   1087     use radroots_event::listing::operational::{OperationalListingBin, OperationalListingProduct};
   1088     use radroots_event::social::group::{
   1089         GroupAdmins, GroupCreateGroup, GroupCreateInvite, GroupDeleteEvent, GroupDeleteGroup,
   1090         GroupEditMetadata, GroupEditableMetadata, GroupJoinRequest, GroupLeaveRequest,
   1091         GroupMembers, GroupMetadata, GroupPutUser, GroupRemoveUser, GroupRole, GroupRoles,
   1092         GroupUserRef,
   1093     };
   1094     use radroots_event::social::http_auth::HttpAuth;
   1095     use radroots_event::social::job::JobInputType;
   1096     use radroots_event::social::job_request::{JobInput, JobParam};
   1097     use radroots_event::social::relay_auth::RelayAuth;
   1098     use radroots_event::social::{
   1099         ReportFileTarget, ReportType, SocialFarmAnchor, SocialLocation, SocialMediaDimensions,
   1100         SocialTarget,
   1101     };
   1102     use radroots_trade::evidence::{
   1103         RadrootsTradeEvidenceManifestSourceResultV1, RadrootsTradeEvidencePolicyDigestV1,
   1104         RadrootsTradeEvidenceScopePrerequisitesV1, RadrootsTradeEvidenceSourceCompletionV1,
   1105         RadrootsTradeEvidenceSourceIdV1, RadrootsTradeEvidenceSourceRequirementV1,
   1106         RadrootsTradeEvidenceSourceResultDigestV1, RadrootsTradeEvidenceSourceResultV1,
   1107     };
   1108 
   1109     fn sample_listing() -> OperationalListing {
   1110         let quantity =
   1111             Quantity::try_new(Decimal::from(1u32), Unit::Each).expect("positive fixture quantity");
   1112         let price = QuantityPrice::try_new(
   1113             Money::try_new(Decimal::from(10u32), Currency::USD)
   1114                 .expect("non-negative fixture money"),
   1115             quantity.clone(),
   1116         )
   1117         .expect("non-zero fixture pricing quantity");
   1118 
   1119         OperationalListing {
   1120             d_tag: "AAAAAAAAAAAAAAAAAAAAAg".parse().expect("listing d tag"),
   1121             published_at: None,
   1122             farm: FarmRef {
   1123                 pubkey: "farm_pubkey".to_string(),
   1124                 d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
   1125             },
   1126             product: OperationalListingProduct {
   1127                 key: "sku".to_string(),
   1128                 title: "widget".to_string(),
   1129                 category: "tools".to_string(),
   1130                 summary: None,
   1131                 process: None,
   1132                 lot: None,
   1133                 location: None,
   1134                 profile: None,
   1135                 year: None,
   1136             },
   1137             primary_bin_id: "bin-1".parse().expect("primary bin id"),
   1138             bins: vec![OperationalListingBin {
   1139                 bin_id: "bin-1".parse().expect("bin id"),
   1140                 quantity,
   1141                 price_per_canonical_unit: price,
   1142                 display_amount: None,
   1143                 display_unit: None,
   1144                 display_label: None,
   1145                 display_price: None,
   1146                 display_price_unit: None,
   1147             }],
   1148             resource_area: None,
   1149             plot: None,
   1150             discounts: None,
   1151             inventory_available: None,
   1152             availability: None,
   1153             delivery_method: None,
   1154             location: None,
   1155             images: None,
   1156         }
   1157     }
   1158 
   1159     fn synthetic_pubkey(seed: char) -> String {
   1160         seed.to_string().repeat(64)
   1161     }
   1162 
   1163     fn synthetic_event_id(seed: char) -> String {
   1164         seed.to_string().repeat(64)
   1165     }
   1166 
   1167     fn social_farm_anchor() -> SocialFarmAnchor {
   1168         SocialFarmAnchor {
   1169             farm: FarmRef {
   1170                 pubkey: synthetic_pubkey('a'),
   1171                 d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
   1172             },
   1173             relays: Some(vec!["wss://relay.example.test".to_string()]),
   1174         }
   1175     }
   1176 
   1177     fn event_target(kind: u32, seed: char) -> SocialTarget {
   1178         SocialTarget::Event {
   1179             id: synthetic_event_id(seed),
   1180             author: Some(synthetic_pubkey('b')),
   1181             event_kind: Some(kind),
   1182             relays: Some(vec!["wss://relay.example.test".to_string()]),
   1183         }
   1184     }
   1185 
   1186     fn address_target(kind: u32, d_tag: &str) -> SocialTarget {
   1187         let author = synthetic_pubkey('c');
   1188         SocialTarget::Address {
   1189             address: format!("{kind}:{author}:{d_tag}"),
   1190             author: Some(author),
   1191             event_kind: Some(kind),
   1192             relays: Some(vec!["wss://relay2.example.test".to_string()]),
   1193         }
   1194     }
   1195 
   1196     fn knowledge_event_ref(seed: char, kind: u32) -> radroots_event::tag::EventRef {
   1197         radroots_event::tag::EventRef {
   1198             id: synthetic_event_id(seed),
   1199             author: radroots_identity::PublicKey::from_hex(&synthetic_pubkey('a'))
   1200                 .expect("fixture public key"),
   1201             kind,
   1202             d_tag: None,
   1203             relays: Some(vec!["wss://relay.example.test".to_string()]),
   1204         }
   1205     }
   1206 
   1207     fn knowledge_address_ref() -> AddressableRef {
   1208         AddressableRef {
   1209             kind: KIND_WIKI_ARTICLE,
   1210             pubkey: synthetic_pubkey('a'),
   1211             d_tag: "soil-health".to_string(),
   1212             relays: vec!["wss://relay.example.test".to_string()],
   1213         }
   1214     }
   1215 
   1216     fn sample_wiki_article() -> WikiArticle {
   1217         WikiArticle {
   1218             d_tag: "soil-health".to_string(),
   1219             title: Some("Soil health".to_string()),
   1220             content_djot: "# Soil health".to_string(),
   1221             summary: Some("Living soil basics".to_string()),
   1222             topics: vec!["soil".to_string()],
   1223             references: vec![knowledge_event_ref('1', KIND_KNOWLEDGE_SOURCE)],
   1224             forked_from: vec![WikiArticleVersionRef {
   1225                 event_id: synthetic_event_id('b'),
   1226                 address_ref: knowledge_address_ref(),
   1227             }],
   1228             deferred_to: None,
   1229         }
   1230     }
   1231 
   1232     fn sample_wiki_redirect() -> WikiRedirect {
   1233         WikiRedirect {
   1234             d_tag: "soil".to_string(),
   1235             target: knowledge_address_ref(),
   1236         }
   1237     }
   1238 
   1239     fn sample_wiki_merge_request() -> WikiMergeRequest {
   1240         WikiMergeRequest {
   1241             target_article: knowledge_address_ref(),
   1242             destination_pubkey: synthetic_pubkey('a'),
   1243             base_version_event_id: Some(synthetic_event_id('e')),
   1244             source_version_event_id: synthetic_event_id('f'),
   1245             explanation: Some("Merge synthetic soil article updates".to_string()),
   1246         }
   1247     }
   1248 
   1249     fn sample_knowledge_source() -> KnowledgeSource {
   1250         KnowledgeSource {
   1251             schema: RADROOTS_KNOWLEDGE_SOURCE_SCHEMA.to_string(),
   1252             schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
   1253             d_tag: "soil-source".to_string(),
   1254             title: "Soil Source".to_string(),
   1255             source_type: "book".to_string(),
   1256             authors: vec!["A. Example".to_string()],
   1257             publisher: Some("Radroots Synthetic Press".to_string()),
   1258             publication_year: Some(2026),
   1259             edition: None,
   1260             canonical_url: Some("https://source.example.test/soil-source".to_string()),
   1261             artifact_refs: vec![knowledge_event_ref('3', KIND_FILE_METADATA)],
   1262             author_asserted_rights: None,
   1263             topics: vec!["soil".to_string()],
   1264             summary: Some("Synthetic source for wasm coverage".to_string()),
   1265         }
   1266     }
   1267 
   1268     fn sample_knowledge_claim() -> KnowledgeClaim {
   1269         KnowledgeClaim {
   1270             schema: RADROOTS_KNOWLEDGE_CLAIM_SCHEMA.to_string(),
   1271             schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
   1272             claim_type: "practice_effect".to_string(),
   1273             text: "Cover crops improve soil structure.".to_string(),
   1274             citation_spans: vec![KnowledgeCitationSpan {
   1275                 source_ref: knowledge_event_ref('4', KIND_KNOWLEDGE_SOURCE),
   1276                 artifact_ref: None,
   1277                 page_start: Some(12),
   1278                 page_end: Some(13),
   1279                 section_path: vec!["chapter-1".to_string()],
   1280                 quote_hash: Some(synthetic_event_id('5')),
   1281                 chunk_id: Some("chunk-1".to_string()),
   1282             }],
   1283             topics: vec!["cover-crops".to_string()],
   1284             applies_to: vec!["local-food".to_string()],
   1285             author_asserted_confidence: Some("medium".to_string()),
   1286             supersedes: Vec::new(),
   1287         }
   1288     }
   1289 
   1290     fn sample_knowledge_node_ref(label: &str) -> KnowledgeNodeRef {
   1291         KnowledgeNodeRef {
   1292             node_type: "event".to_string(),
   1293             event_ref: Some(knowledge_event_ref('6', KIND_KNOWLEDGE_CLAIM)),
   1294             address_ref: None,
   1295             external_id: None,
   1296             label: Some(label.to_string()),
   1297         }
   1298     }
   1299 
   1300     fn sample_knowledge_relation() -> KnowledgeRelation {
   1301         KnowledgeRelation {
   1302             schema: RADROOTS_KNOWLEDGE_RELATION_SCHEMA.to_string(),
   1303             schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
   1304             subject: sample_knowledge_node_ref("cover crops"),
   1305             predicate: "supports".to_string(),
   1306             object: sample_knowledge_node_ref("soil structure"),
   1307             support_refs: vec![knowledge_event_ref('7', KIND_KNOWLEDGE_CLAIM)],
   1308             author_asserted_confidence: Some("medium".to_string()),
   1309             supersedes: Vec::new(),
   1310         }
   1311     }
   1312 
   1313     fn sample_knowledge_review() -> KnowledgeReview {
   1314         KnowledgeReview {
   1315             schema: RADROOTS_KNOWLEDGE_REVIEW_SCHEMA.to_string(),
   1316             schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
   1317             target: KnowledgeReviewTarget {
   1318                 event_id: synthetic_event_id('8'),
   1319                 author_pubkey: synthetic_pubkey('a'),
   1320                 kind: KIND_KNOWLEDGE_CLAIM,
   1321                 address: None,
   1322                 relays: vec!["wss://relay.example.test".to_string()],
   1323                 review_scope: KnowledgeReviewScope::SpecificVersion,
   1324             },
   1325             reviewer_role: "peer".to_string(),
   1326             verdict: "needs_more_evidence".to_string(),
   1327             scores: vec![KnowledgeReviewScore {
   1328                 dimension: "evidence".to_string(),
   1329                 value: "partial".to_string(),
   1330                 note: None,
   1331             }],
   1332             notes: Some("Synthetic review".to_string()),
   1333             evidence_refs: vec![knowledge_event_ref('9', KIND_KNOWLEDGE_SOURCE)],
   1334         }
   1335     }
   1336 
   1337     fn sample_knowledge_field_report() -> KnowledgeFieldReport {
   1338         KnowledgeFieldReport {
   1339             schema: RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA.to_string(),
   1340             schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION,
   1341             report_type: "observation".to_string(),
   1342             title: "Field observation".to_string(),
   1343             summary: Some("Observed cover crop residue.".to_string()),
   1344             context: KnowledgeFieldContext {
   1345                 location_precision: KnowledgeLocationPrecision::CoarseGeohash,
   1346                 public_location: Some(KnowledgeLocation {
   1347                     label: Some("watershed".to_string()),
   1348                     region: Some("synthetic-region".to_string()),
   1349                     locality: None,
   1350                     geohash: Some("c23".to_string()),
   1351                 }),
   1352                 private_location_ref: None,
   1353                 topics: vec!["field".to_string()],
   1354                 context_tags: vec!["observation".to_string()],
   1355             },
   1356             observations: vec![KnowledgeObservation {
   1357                 observation_type: "residue".to_string(),
   1358                 text: "Residue was visible across beds.".to_string(),
   1359                 observed_at: Some("2026-07-05".to_string()),
   1360                 values: vec![KnowledgeObservationValue {
   1361                     key: "coverage".to_string(),
   1362                     value: "medium".to_string(),
   1363                     unit: None,
   1364                 }],
   1365             }],
   1366             artifact_refs: vec![knowledge_event_ref('c', KIND_FILE_METADATA)],
   1367             related_refs: vec![knowledge_event_ref('d', KIND_KNOWLEDGE_CLAIM)],
   1368             limitations: vec!["single observer".to_string()],
   1369         }
   1370     }
   1371 
   1372     fn signed_claim_event_json() -> String {
   1373         let claim_json = serde_json::to_string(&sample_knowledge_claim()).expect("claim json");
   1374         let tags = serde_json::from_str::<Vec<Vec<String>>>(
   1375             &knowledge_claim_tags(&claim_json).expect("claim tags"),
   1376         )
   1377         .expect("tags");
   1378         let tags = tags
   1379             .into_iter()
   1380             .map(nostr::Tag::parse)
   1381             .collect::<Result<Vec<_>, _>>()
   1382             .expect("parsed tags");
   1383         let keys = nostr::Keys::generate();
   1384         let event =
   1385             nostr::EventBuilder::new(nostr::Kind::Custom(KIND_KNOWLEDGE_CLAIM as u16), claim_json)
   1386                 .tags(tags)
   1387                 .custom_created_at(nostr::Timestamp::from_secs(1_800_000_000))
   1388                 .sign_with_keys(&keys)
   1389                 .expect("signed event");
   1390         let envelope = EventEnvelope::new(EventEnvelopeParts {
   1391             id: event.id.to_hex(),
   1392             author: event.pubkey.to_hex(),
   1393             created_at: event.created_at.as_secs(),
   1394             kind: u32::from(event.kind.as_u16()),
   1395             tags: event
   1396                 .tags
   1397                 .as_slice()
   1398                 .iter()
   1399                 .map(|tag| tag.as_slice().to_vec())
   1400                 .collect(),
   1401             content: event.content,
   1402             sig: event.sig.to_string(),
   1403         })
   1404         .expect("event envelope");
   1405         serde_json::to_string(&envelope).expect("event json")
   1406     }
   1407 
   1408     fn social_location() -> SocialLocation {
   1409         SocialLocation {
   1410             name: Some("field edge".to_string()),
   1411             geohash: Some("c23nb62w20st".to_string()),
   1412         }
   1413     }
   1414 
   1415     fn sample_article() -> Article {
   1416         Article {
   1417             d_tag: "AAAAAAAAAAAAAAAAAAAAAg".to_string(),
   1418             title: "soil notes".to_string(),
   1419             content: "# soil notes".to_string(),
   1420             summary: Some("cover crop observations".to_string()),
   1421             image: Some("https://media.example.test/article.jpg".to_string()),
   1422             published_at: Some(1_780_000_000),
   1423             farm: Some(social_farm_anchor()),
   1424             location: Some(social_location()),
   1425             topics: Some(vec!["soil".to_string(), "cover-crops".to_string()]),
   1426         }
   1427     }
   1428 
   1429     fn sample_public_file_metadata() -> FileMetadata {
   1430         FileMetadata {
   1431             url: "https://media.example.test/public.jpg".to_string(),
   1432             mime_type: "image/jpeg".to_string(),
   1433             sha256: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
   1434             original_sha256: Some(
   1435                 "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".to_string(),
   1436             ),
   1437             size: Some(4096),
   1438             dimensions: Some(SocialMediaDimensions {
   1439                 width: 1200,
   1440                 height: 800,
   1441             }),
   1442             blurhash: None,
   1443             thumbnails: None,
   1444             summary: Some("public field photo".to_string()),
   1445             alt: Some("rows after harvest".to_string()),
   1446             fallback: Some("https://media.example.test/fallback.jpg".to_string()),
   1447             magnet: Some("magnet:?xt=urn:btih:abc".to_string()),
   1448             content_hashes: Some(vec!["sha256:field".to_string()]),
   1449             services: Some(vec!["https://media.example.test".to_string()]),
   1450             content: Some("caption".to_string()),
   1451         }
   1452     }
   1453 
   1454     fn sample_reaction() -> Reaction {
   1455         Reaction {
   1456             target: address_target(30023, "AAAAAAAAAAAAAAAAAAAAAg"),
   1457             content: String::new(),
   1458         }
   1459     }
   1460 
   1461     fn sample_repost() -> Repost {
   1462         Repost {
   1463             target: event_target(1, 'b'),
   1464             content: Some("field update".to_string()),
   1465         }
   1466     }
   1467 
   1468     fn sample_generic_repost() -> GenericRepost {
   1469         GenericRepost {
   1470             target: address_target(30023, "AAAAAAAAAAAAAAAAAAAAAg"),
   1471             target_kind: 30023,
   1472             content: Some("article share".to_string()),
   1473         }
   1474     }
   1475 
   1476     fn sample_report() -> Report {
   1477         Report {
   1478             reported_pubkey: synthetic_pubkey('b'),
   1479             report_type: ReportType::Spam,
   1480             event: Some(event_target(1, 'c')),
   1481             file: Some(ReportFileTarget {
   1482                 sha256: Some(
   1483                     "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
   1484                 ),
   1485                 url: Some("https://media.example.test/bad.jpg".to_string()),
   1486                 magnet: None,
   1487             }),
   1488             content: Some("spam report".to_string()),
   1489         }
   1490     }
   1491 
   1492     fn sample_job_request() -> JobRequest {
   1493         JobRequest {
   1494             kind: 5100,
   1495             inputs: vec![JobInput {
   1496                 data: "alpha".to_string(),
   1497                 input_type: JobInputType::Text,
   1498                 relay: None,
   1499                 marker: None,
   1500             }],
   1501             output: None,
   1502             params: vec![JobParam {
   1503                 key: "mode".to_string(),
   1504                 value: "fast".to_string(),
   1505             }],
   1506             bid_sat: Some(42),
   1507             relays: vec!["wss://relay.example.com".to_string()],
   1508             providers: vec!["provider-a".to_string()],
   1509             topics: vec!["topic-a".to_string()],
   1510             encrypted: false,
   1511         }
   1512     }
   1513 
   1514     fn sample_workspace_manifest() -> FarmWorkspaceManifest {
   1515         FarmWorkspaceManifest {
   1516             d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
   1517             schema: RADROOTS_FARM_WORKSPACE_SCHEMA.to_string(),
   1518             farm_group_id: "field-group".to_string(),
   1519             name: "Small Regen Farm".to_string(),
   1520             owner_pubkey: "workspace_owner_pubkey".to_string(),
   1521             farm: Some(FarmRef {
   1522                 pubkey: "farm_pubkey".to_string(),
   1523                 d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(),
   1524             }),
   1525             relays: vec![FarmWorkspaceRelay {
   1526                 url: "wss://relay.example.invalid/farm/field-group".to_string(),
   1527                 mode: FarmWorkspaceRelayMode::ReadWrite,
   1528             }],
   1529             media_servers: vec![FarmWorkspaceMediaServer {
   1530                 url: "https://media.example.invalid/farm/field-group".to_string(),
   1531                 service: "RadrootsPrivateMedia".to_string(),
   1532             }],
   1533             supported_kinds: vec![78, 30078, KIND_FARM_FILE_METADATA],
   1534             protocol_version: RADROOTS_FARM_WORKSPACE_PROTOCOL_VERSION.to_string(),
   1535             created_at_ms: 1_780_000_000_000,
   1536             updated_at_ms: None,
   1537         }
   1538     }
   1539 
   1540     fn sample_crdt_change() -> FarmCrdtChange {
   1541         FarmCrdtChange {
   1542             schema: RADROOTS_FARM_CRDT_CHANGE_SCHEMA.to_string(),
   1543             workspace: FarmWorkspaceRef {
   1544                 pubkey: "workspace_pubkey".to_string(),
   1545                 d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
   1546             },
   1547             farm_group_id: "field-group".to_string(),
   1548             document_id: "AAAAAAAAAAAAAAAAAAAAAg".to_string(),
   1549             document_kind: FarmCrdtDocumentKind::FarmTask,
   1550             crdt_backend: CrdtBackend::Automerge,
   1551             crdt_backend_version: Some("0.x".to_string()),
   1552             actor_id: "actor_abc".to_string(),
   1553             change_hash: "crdt_hash_abc".to_string(),
   1554             dependencies: Vec::new(),
   1555             encoded_change: "abc-DEF_012".to_string(),
   1556             semantic_kind: FarmSemanticKind::FarmTaskCreate,
   1557             business_time_ms: 1_780_000_000_000,
   1558             author_member_id: Some("member_abc".to_string()),
   1559             app_version: Some("0.1.0".to_string()),
   1560         }
   1561     }
   1562 
   1563     fn sample_file_metadata() -> FarmFileMetadata {
   1564         FarmFileMetadata {
   1565             d_tag: "AAAAAAAAAAAAAAAAAAAAAQ".to_string(),
   1566             workspace: FarmWorkspaceRef {
   1567                 pubkey: "workspace_pubkey".to_string(),
   1568                 d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
   1569             },
   1570             farm_group_id: "field-group".to_string(),
   1571             owner_document_id: "AAAAAAAAAAAAAAAAAAAAAg".to_string(),
   1572             owner_document_kind: FarmCrdtDocumentKind::FarmTask,
   1573             caption: Some("Tomatoes harvested from Patch Y.".to_string()),
   1574             url: "https://media.example.invalid/blob/sha256".to_string(),
   1575             mime_type: "image/jpeg".to_string(),
   1576             sha256: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
   1577             original_sha256: None,
   1578             size_bytes: Some(123_456),
   1579             dimensions: Some(FarmFileDimensions { w: 1600, h: 1200 }),
   1580             blurhash: None,
   1581             thumb: Some(FarmFileSource {
   1582                 url: "https://media.example.invalid/thumb/sha256".to_string(),
   1583                 mime_type: Some("image/jpeg".to_string()),
   1584                 dimensions: Some(FarmFileDimensions { w: 320, h: 240 }),
   1585             }),
   1586             image: None,
   1587             alt: Some("Harvested tomatoes in a crate".to_string()),
   1588             fallbacks: Vec::new(),
   1589         }
   1590     }
   1591 
   1592     fn sample_group_metadata() -> GroupEditableMetadata {
   1593         GroupEditableMetadata {
   1594             name: Some("Small Regen Farm".to_string()),
   1595             about: Some("Field app group".to_string()),
   1596             picture: Some("https://media.example.invalid/group.png".to_string()),
   1597             is_private: false,
   1598             is_restricted: true,
   1599             is_closed: false,
   1600             is_hidden: false,
   1601             supported_kinds: Some(vec![78, 30078, KIND_FARM_FILE_METADATA]),
   1602         }
   1603     }
   1604 
   1605     fn sample_group_user(role: &str) -> GroupUserRef {
   1606         GroupUserRef {
   1607             pubkey: format!("{role}_pubkey"),
   1608             roles: vec![role.to_string()],
   1609         }
   1610     }
   1611 
   1612     fn sample_group_role() -> GroupRole {
   1613         GroupRole {
   1614             name: "member".to_string(),
   1615             description: Some("can read and write group events".to_string()),
   1616             permissions: vec!["read".to_string(), "write".to_string()],
   1617         }
   1618     }
   1619 
   1620     fn assert_tags_json(value: Result<String, RadrootsJsValue>) {
   1621         let tags = tags_json(value);
   1622         assert!(!tags.is_empty());
   1623     }
   1624 
   1625     fn tags_json(value: Result<String, RadrootsJsValue>) -> Vec<Vec<String>> {
   1626         let json = value.expect("tags json");
   1627         serde_json::from_str(&json).expect("tags")
   1628     }
   1629 
   1630     fn has_tag(tags: &[Vec<String>], key: &str, value: &str) -> bool {
   1631         tags.iter().any(|tag| {
   1632             tag.first().map(|entry| entry.as_str()) == Some(key)
   1633                 && tag.get(1).map(|entry| entry.as_str()) == Some(value)
   1634         })
   1635     }
   1636 
   1637     fn has_exact_tag(tags: &[Vec<String>], expected: &[&str]) -> bool {
   1638         tags.iter().any(|tag| {
   1639             tag.iter()
   1640                 .map(|entry| entry.as_str())
   1641                 .eq(expected.iter().copied())
   1642         })
   1643     }
   1644 
   1645     type BindingEncoder = fn(&str) -> Result<String, RadrootsJsValue>;
   1646 
   1647     const POST_AUTHORED_VECTORS: &str =
   1648         include_str!("../../../contracts/conformance/vectors/post/verified_profiles.v1.json");
   1649     const COMMENT_AUTHORED_VECTORS: &str =
   1650         include_str!("../../../contracts/conformance/vectors/comment/verified_profile.v1.json");
   1651 
   1652     #[derive(Deserialize)]
   1653     #[serde(deny_unknown_fields)]
   1654     struct AuthoredVectorSuite {
   1655         suite: String,
   1656         contract_version: String,
   1657         vectors: Vec<AuthoredVector>,
   1658     }
   1659 
   1660     #[derive(Deserialize)]
   1661     #[serde(deny_unknown_fields)]
   1662     struct AuthoredVector {
   1663         id: String,
   1664         kind: String,
   1665         input: serde_json::Value,
   1666         expected: serde_json::Value,
   1667     }
   1668 
   1669     fn authored_binding(kind: &str) -> Option<BindingEncoder> {
   1670         if kind.starts_with("social.update.build_authored_draft.") {
   1671             Some(social_update_build_authored_draft)
   1672         } else if kind.starts_with("social.photo_update.build_authored_draft.") {
   1673             Some(social_photo_update_build_authored_draft)
   1674         } else if kind.starts_with("social.ask.build_authored_draft.") {
   1675             Some(social_ask_build_authored_draft)
   1676         } else if kind.starts_with("social.comment.build_authored_draft.") {
   1677             Some(social_comment_build_authored_draft)
   1678         } else {
   1679             None
   1680         }
   1681     }
   1682 
   1683     fn authored_vector_request(vector: &AuthoredVector) -> String {
   1684         let mut input = vector.input.clone();
   1685         if let Some(images) = input
   1686             .get_mut("images")
   1687             .and_then(serde_json::Value::as_array_mut)
   1688         {
   1689             for image in images {
   1690                 let image = image
   1691                     .as_object_mut()
   1692                     .unwrap_or_else(|| panic!("{} image must be an object", vector.id));
   1693                 let bytes_utf8 = image
   1694                     .remove("bytes_utf8")
   1695                     .unwrap_or_else(|| panic!("{} image must carry vector bytes", vector.id));
   1696                 let bytes_utf8 = bytes_utf8
   1697                     .as_str()
   1698                     .unwrap_or_else(|| panic!("{} vector bytes must be UTF-8 text", vector.id));
   1699                 image.insert(
   1700                     "bytes".to_owned(),
   1701                     serde_json::Value::Array(
   1702                         bytes_utf8
   1703                             .as_bytes()
   1704                             .iter()
   1705                             .copied()
   1706                             .map(serde_json::Value::from)
   1707                             .collect(),
   1708                     ),
   1709                 );
   1710             }
   1711         }
   1712         serde_json::to_string(&input)
   1713             .unwrap_or_else(|error| panic!("{} request JSON failed: {error}", vector.id))
   1714     }
   1715 
   1716     fn authored_error_value(error: RadrootsJsValue) -> serde_json::Value {
   1717         serde_json::from_str(&error).expect("authored binding error must be coded JSON")
   1718     }
   1719 
   1720     fn authored_error_inner(error: RadrootsJsValue) -> String {
   1721         let error = authored_error_value(error);
   1722         assert_eq!(error["code"], "encode_error");
   1723         error["inner_code"]
   1724             .as_str()
   1725             .expect("authored error inner_code")
   1726             .to_owned()
   1727     }
   1728 
   1729     fn execute_authored_vectors(raw: &str, expected_suite: &str, expected_vector_count: usize) {
   1730         let suite: AuthoredVectorSuite =
   1731             serde_json::from_str(raw).expect("owner conformance vectors");
   1732         assert_eq!(suite.suite, expected_suite);
   1733         assert_eq!(suite.contract_version, "1.0.0");
   1734         let mut executed = 0usize;
   1735         for vector in suite.vectors {
   1736             let Some(binding) = authored_binding(&vector.kind) else {
   1737                 continue;
   1738             };
   1739             executed += 1;
   1740             let request = authored_vector_request(&vector);
   1741             if vector.kind.ends_with(".valid") {
   1742                 let first = binding(&request)
   1743                     .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id));
   1744                 let second = binding(&request)
   1745                     .unwrap_or_else(|error| panic!("{} repeat failed: {error}", vector.id));
   1746                 assert_eq!(first, second, "{} repeat encoding drifted", vector.id);
   1747                 let actual: serde_json::Value = serde_json::from_str(&first)
   1748                     .unwrap_or_else(|error| panic!("{} output JSON failed: {error}", vector.id));
   1749                 assert_eq!(actual, vector.expected, "{}", vector.id);
   1750             } else if vector.kind.ends_with(".invalid") {
   1751                 let first = binding(&request).expect_err("invalid authored vector must fail");
   1752                 let second =
   1753                     binding(&request).expect_err("repeated invalid authored vector must fail");
   1754                 let expected = vector.expected["error"]
   1755                     .as_str()
   1756                     .unwrap_or_else(|| panic!("{} expected error must be a string", vector.id));
   1757                 assert_eq!(authored_error_inner(first), expected, "{}", vector.id);
   1758                 assert_eq!(authored_error_inner(second), expected, "{}", vector.id);
   1759             } else {
   1760                 panic!("{} has unsupported authored vector kind", vector.id);
   1761             }
   1762         }
   1763         assert_eq!(executed, expected_vector_count);
   1764     }
   1765 
   1766     #[test]
   1767     fn bindings_reject_invalid_json() {
   1768         let bindings: [BindingEncoder; 44] = [
   1769             social_update_build_authored_draft,
   1770             social_photo_update_build_authored_draft,
   1771             social_ask_build_authored_draft,
   1772             social_comment_build_authored_draft,
   1773             operational_listing_tags,
   1774             operational_listing_tags_full,
   1775             article_tags,
   1776             file_metadata_tags,
   1777             repost_tags,
   1778             generic_repost_tags,
   1779             report_tags,
   1780             follow_tags,
   1781             document_tags,
   1782             coop_tags,
   1783             farm_tags,
   1784             list_tags,
   1785             list_set_tags,
   1786             plot_tags,
   1787             job_request_tags,
   1788             job_result_tags,
   1789             job_feedback_tags,
   1790             reaction_tags,
   1791             message_tags,
   1792             message_file_tags,
   1793             seal_tags,
   1794             gift_wrap_tags,
   1795             farm_workspace_manifest_tags,
   1796             farm_crdt_change_tags,
   1797             farm_file_metadata_tags,
   1798             relay_auth_tags,
   1799             http_auth_tags,
   1800             group_put_user_tags,
   1801             group_remove_user_tags,
   1802             group_create_group_tags,
   1803             group_edit_metadata_tags,
   1804             group_delete_group_tags,
   1805             group_delete_event_tags,
   1806             group_create_invite_tags,
   1807             group_join_request_tags,
   1808             group_leave_request_tags,
   1809             group_metadata_tags,
   1810             group_admins_tags,
   1811             group_members_tags,
   1812             group_roles_tags,
   1813         ];
   1814 
   1815         for binding in bindings {
   1816             assert!(binding("{").is_err());
   1817         }
   1818         assert!(operational_listing_tags("").is_err());
   1819     }
   1820 
   1821     #[test]
   1822     fn strict_authored_post_bindings_execute_owner_conformance_vectors() {
   1823         execute_authored_vectors(POST_AUTHORED_VECTORS, "post_profiles", 6);
   1824     }
   1825 
   1826     #[test]
   1827     fn strict_authored_comment_binding_executes_owner_conformance_vectors() {
   1828         execute_authored_vectors(COMMENT_AUTHORED_VECTORS, "nip22_comment_profile", 31);
   1829     }
   1830 
   1831     #[test]
   1832     fn authored_comment_rejects_address_coordinate_author_and_kind_mismatches() {
   1833         let author = synthetic_pubkey('e');
   1834         let coordinate = format!("30402:{author}:victoria-market");
   1835         let request = |author: &str, kind: u32| {
   1836             serde_json::json!({
   1837                 "content": "Available this week",
   1838                 "root": {
   1839                     "type": "address",
   1840                     "coordinate": coordinate,
   1841                     "author": author,
   1842                     "kind": kind,
   1843                     "relay": null,
   1844                 },
   1845                 "position": {
   1846                     "type": "top_address",
   1847                     "current_revision": synthetic_event_id('b'),
   1848                 },
   1849             })
   1850             .to_string()
   1851         };
   1852 
   1853         let author_error =
   1854             social_comment_build_authored_draft(&request(&synthetic_pubkey('f'), 30402))
   1855                 .expect_err("coordinate author mismatch");
   1856         assert_eq!(
   1857             authored_error_inner(author_error),
   1858             "comment_root_author_mismatch"
   1859         );
   1860 
   1861         let kind_error = social_comment_build_authored_draft(&request(&author, 31922))
   1862             .expect_err("coordinate kind mismatch");
   1863         assert_eq!(
   1864             authored_error_inner(kind_error),
   1865             "comment_root_kind_mismatch"
   1866         );
   1867     }
   1868 
   1869     #[test]
   1870     fn authored_image_errors_keep_the_stable_binding_code() {
   1871         let error = image_authored_error(AuthoredImageError::MediaTypeNotImage);
   1872         assert_eq!(authored_error_inner(error), "media_type_not_image");
   1873     }
   1874 
   1875     #[test]
   1876     fn authored_media_binding_verifies_exact_arbitrary_bytes() {
   1877         let bytes = vec![0, 159, 146, 150];
   1878         let hash = Sha256::digest(&bytes);
   1879         let url = format!("https://media.example/{hash}.webp");
   1880         let input = serde_json::json!({
   1881             "content": format!("Harvest {url}"),
   1882             "images": [{
   1883                 "bytes": bytes,
   1884                 "url": url,
   1885                 "media_type": "image/webp",
   1886                 "uploaded": 1_784_347_200_u64,
   1887                 "width": 1200,
   1888                 "height": 900,
   1889                 "alt": "Harvest",
   1890                 "fallbacks": [],
   1891             }],
   1892         });
   1893 
   1894         let encoded = social_photo_update_build_authored_draft(
   1895             &serde_json::to_string(&input).expect("media request"),
   1896         )
   1897         .expect("byte-verified PhotoUpdate");
   1898         let encoded: Nip01EventWireParts = serde_json::from_str(&encoded).expect("wire parts");
   1899         assert_eq!(encoded.tags.len(), 1);
   1900         assert!(encoded.tags[0].contains(&format!("x {hash}")));
   1901         assert!(encoded.tags[0].contains(&"size 4".to_owned()));
   1902 
   1903         let mut wrong_bytes = input.clone();
   1904         wrong_bytes["images"][0]["bytes"] = serde_json::json!([1, 2, 3, 4]);
   1905         let error = social_photo_update_build_authored_draft(
   1906             &serde_json::to_string(&wrong_bytes).expect("wrong-byte request"),
   1907         )
   1908         .expect_err("URL digest must bind exact bytes");
   1909         assert_eq!(authored_error_inner(error), "descriptor_hash_mismatch");
   1910     }
   1911 
   1912     #[test]
   1913     fn authored_media_binding_rejects_fabricated_descriptor_fields() {
   1914         let bytes = b"image".to_vec();
   1915         let hash = Sha256::digest(&bytes);
   1916         let url = format!("https://media.example/{hash}.webp");
   1917         let mut input = serde_json::json!({
   1918             "content": format!("Harvest {url}"),
   1919             "images": [{
   1920                 "bytes": bytes,
   1921                 "url": url,
   1922                 "media_type": "image/webp",
   1923                 "uploaded": 1_784_347_200_u64,
   1924                 "width": 1200,
   1925                 "height": 900,
   1926                 "alt": "Harvest",
   1927             }],
   1928         });
   1929         input["images"][0]["sha256"] = serde_json::json!(hash.to_string());
   1930         input["images"][0]["size"] = serde_json::json!(5);
   1931 
   1932         let error = social_photo_update_build_authored_draft(
   1933             &serde_json::to_string(&input).expect("fabricated descriptor request"),
   1934         )
   1935         .expect_err("caller-supplied descriptor commitments must be rejected");
   1936         let error = authored_error_value(error);
   1937         assert_eq!(error["code"], "invalid_json");
   1938         assert_eq!(error["inner_code"], "authored_input");
   1939     }
   1940 
   1941     #[test]
   1942     fn ask_allows_zero_media_while_photo_update_requires_media() {
   1943         let ask = social_ask_build_authored_draft(r#"{"content":"When is harvest?","images":[]}"#)
   1944             .expect("Ask without media");
   1945         let ask: Nip01EventWireParts = serde_json::from_str(&ask).expect("Ask wire parts");
   1946         assert_eq!(
   1947             ask.tags,
   1948             vec![vec!["t".to_owned(), "radroots-ask".to_owned()]]
   1949         );
   1950 
   1951         let error =
   1952             social_photo_update_build_authored_draft(r#"{"content":"Harvest","images":[]}"#)
   1953                 .expect_err("PhotoUpdate without media");
   1954         assert_eq!(authored_error_inner(error), "photo_imeta_missing");
   1955     }
   1956 
   1957     #[test]
   1958     fn bindings_encode_to_json_when_input_is_valid() {
   1959         let listing_json = serde_json::to_string(&sample_listing()).expect("listing json");
   1960         let listing_tags_json =
   1961             operational_listing_tags(&listing_json).expect("operational listing tags");
   1962         let listing_tags: Vec<Vec<String>> =
   1963             serde_json::from_str(&listing_tags_json).expect("listing tags json");
   1964         assert!(!listing_tags.is_empty());
   1965 
   1966         let request_json = serde_json::to_string(&sample_job_request()).expect("request json");
   1967         let request_tags_json = job_request_tags(&request_json).expect("request tags");
   1968         let request_tags: Vec<Vec<String>> =
   1969             serde_json::from_str(&request_tags_json).expect("request tags json");
   1970         assert!(!request_tags.is_empty());
   1971     }
   1972 
   1973     #[test]
   1974     fn social_bindings_encode_to_json_when_input_is_valid() {
   1975         assert_tags_json(article_tags(
   1976             &serde_json::to_string(&sample_article()).expect("article json"),
   1977         ));
   1978         assert_tags_json(file_metadata_tags(
   1979             &serde_json::to_string(&sample_public_file_metadata()).expect("file json"),
   1980         ));
   1981         assert_tags_json(reaction_tags(
   1982             &serde_json::to_string(&sample_reaction()).expect("reaction json"),
   1983         ));
   1984         assert_tags_json(repost_tags(
   1985             &serde_json::to_string(&sample_repost()).expect("repost json"),
   1986         ));
   1987         assert_tags_json(generic_repost_tags(
   1988             &serde_json::to_string(&sample_generic_repost()).expect("generic repost json"),
   1989         ));
   1990         assert_tags_json(report_tags(
   1991             &serde_json::to_string(&sample_report()).expect("report json"),
   1992         ));
   1993     }
   1994 
   1995     #[test]
   1996     fn social_bindings_surface_builder_errors() {
   1997         let mut article = sample_article();
   1998         article.d_tag.clear();
   1999         assert!(article_tags(&serde_json::to_string(&article).expect("article json")).is_err());
   2000 
   2001         let mut reaction = sample_reaction();
   2002         reaction.target = SocialTarget::External {
   2003             id: "https://example.test/object".to_string(),
   2004             external_kind: "web".to_string(),
   2005             hint: None,
   2006         };
   2007         assert!(reaction_tags(&serde_json::to_string(&reaction).expect("reaction json")).is_err());
   2008 
   2009         let mut report = sample_report();
   2010         report.reported_pubkey.clear();
   2011         assert!(report_tags(&serde_json::to_string(&report).expect("report json")).is_err());
   2012     }
   2013 
   2014     #[test]
   2015     fn knowledge_bindings_encode_to_json_when_input_is_valid() {
   2016         let article_tags = tags_json(wiki_article_tags(
   2017             &serde_json::to_string(&sample_wiki_article()).expect("wiki article json"),
   2018         ));
   2019         assert!(has_tag(&article_tags, "title", "Soil health"));
   2020         let fork_address = format!(
   2021             "{}:{}:soil-health",
   2022             KIND_WIKI_ARTICLE,
   2023             synthetic_pubkey('a')
   2024         );
   2025         let fork_event_id = synthetic_event_id('b');
   2026         assert!(has_exact_tag(
   2027             &article_tags,
   2028             &[
   2029                 "a",
   2030                 fork_address.as_str(),
   2031                 "wss://relay.example.test",
   2032                 "fork"
   2033             ]
   2034         ));
   2035         assert!(has_exact_tag(
   2036             &article_tags,
   2037             &[
   2038                 "e",
   2039                 fork_event_id.as_str(),
   2040                 "wss://relay.example.test",
   2041                 "fork"
   2042             ]
   2043         ));
   2044 
   2045         let redirect_tags = tags_json(wiki_redirect_tags(
   2046             &serde_json::to_string(&sample_wiki_redirect()).expect("wiki redirect json"),
   2047         ));
   2048         let redirect_address = format!(
   2049             "{}:{}:soil-health",
   2050             KIND_WIKI_ARTICLE,
   2051             synthetic_pubkey('a')
   2052         );
   2053         assert!(has_exact_tag(
   2054             &redirect_tags,
   2055             &["a", redirect_address.as_str(), "wss://relay.example.test"]
   2056         ));
   2057 
   2058         let merge_request = sample_wiki_merge_request();
   2059         let merge_parts =
   2060             radroots_event_codec::encode::knowledge::wiki_merge_request_to_wire_parts(
   2061                 &merge_request,
   2062             )
   2063             .expect("merge request parts");
   2064         assert_eq!(merge_parts.content, "Merge synthetic soil article updates");
   2065         let merge_tags = tags_json(wiki_merge_request_tags(
   2066             &serde_json::to_string(&sample_wiki_merge_request()).expect("merge request json"),
   2067         ));
   2068         let source_event_id = synthetic_event_id('f');
   2069         assert!(has_exact_tag(
   2070             &merge_tags,
   2071             &["e", source_event_id.as_str(), "", "source"]
   2072         ));
   2073 
   2074         let source_tags = tags_json(knowledge_source_tags(
   2075             &serde_json::to_string(&sample_knowledge_source()).expect("source json"),
   2076         ));
   2077         assert!(has_tag(
   2078             &source_tags,
   2079             "contract",
   2080             RADROOTS_KNOWLEDGE_SOURCE_SCHEMA
   2081         ));
   2082 
   2083         let claim_tags = tags_json(knowledge_claim_tags(
   2084             &serde_json::to_string(&sample_knowledge_claim()).expect("claim json"),
   2085         ));
   2086         assert!(has_tag(
   2087             &claim_tags,
   2088             "contract",
   2089             RADROOTS_KNOWLEDGE_CLAIM_SCHEMA
   2090         ));
   2091 
   2092         assert_tags_json(knowledge_relation_tags(
   2093             &serde_json::to_string(&sample_knowledge_relation()).expect("relation json"),
   2094         ));
   2095         assert_tags_json(knowledge_review_tags(
   2096             &serde_json::to_string(&sample_knowledge_review()).expect("review json"),
   2097         ));
   2098         assert_tags_json(knowledge_field_report_tags(
   2099             &serde_json::to_string(&sample_knowledge_field_report()).expect("field report json"),
   2100         ));
   2101     }
   2102 
   2103     #[test]
   2104     fn wiki_article_tags_accept_missing_title() {
   2105         let mut article = sample_wiki_article();
   2106         article.title = None;
   2107         let tags = tags_json(wiki_article_tags(
   2108             &serde_json::to_string(&article).expect("wiki article json"),
   2109         ));
   2110         assert!(
   2111             !tags
   2112                 .iter()
   2113                 .any(|tag| tag.first().map(String::as_str) == Some("title"))
   2114         );
   2115     }
   2116 
   2117     #[test]
   2118     fn knowledge_claim_tags_enforce_citation_rules() {
   2119         let mut claim = sample_knowledge_claim();
   2120         claim.citation_spans.clear();
   2121         let error = knowledge_claim_tags(
   2122             &serde_json::to_string(&claim).expect("uncited source-backed claim json"),
   2123         )
   2124         .expect_err("source-backed claim requires citations");
   2125         assert!(error.contains("citation_spans"));
   2126 
   2127         assert_tags_json(knowledge_claim_tags(
   2128             &serde_json::to_string(&sample_knowledge_claim()).expect("claim json"),
   2129         ));
   2130 
   2131         for claim_type in ["hypothesis", "observation", "question"] {
   2132             let mut uncited = sample_knowledge_claim();
   2133             uncited.claim_type = claim_type.to_string();
   2134             uncited.citation_spans.clear();
   2135             assert_tags_json(knowledge_claim_tags(
   2136                 &serde_json::to_string(&uncited).expect("uncited claim json"),
   2137             ));
   2138         }
   2139     }
   2140 
   2141     #[test]
   2142     fn knowledge_bindings_verify_decode_and_manifest_json() {
   2143         let decoded = verify_and_decode_event_json(&signed_claim_event_json()).expect("decoded");
   2144         let decoded: serde_json::Value = serde_json::from_str(&decoded).expect("decoded json");
   2145         assert_eq!(decoded["event_type"], "knowledge_claim");
   2146         assert_eq!(decoded["contract_id"], RADROOTS_KNOWLEDGE_CLAIM_SCHEMA);
   2147         assert_eq!(
   2148             decoded["payload"]["text"],
   2149             "Cover crops improve soil structure."
   2150         );
   2151 
   2152         let manifest = contract_manifest_json().expect("manifest");
   2153         let manifest: serde_json::Value = serde_json::from_str(&manifest).expect("manifest json");
   2154         assert_eq!(manifest["schema_version"], 2);
   2155         assert_eq!(manifest["contract_count"], 11);
   2156         let claim_contract = manifest["contracts"]
   2157             .as_array()
   2158             .expect("contracts")
   2159             .iter()
   2160             .find(|contract| contract["contract_id"] == RADROOTS_KNOWLEDGE_CLAIM_SCHEMA)
   2161             .expect("claim contract");
   2162         assert_eq!(claim_contract["sdk_builder_support"], true);
   2163         assert_eq!(claim_contract["sdk_draft_support"], true);
   2164         assert_eq!(claim_contract["wasm_tag_builder_support"], true);
   2165         assert_eq!(claim_contract["wasm_verified_decode_support"], true);
   2166     }
   2167 
   2168     #[test]
   2169     fn knowledge_bindings_verify_decode_errors_are_coded_json() {
   2170         let invalid_json = verify_and_decode_event_json("not json").expect_err("invalid json");
   2171         let invalid_json: serde_json::Value =
   2172             serde_json::from_str(&invalid_json).expect("error json");
   2173         assert_eq!(invalid_json["code"], "invalid_json");
   2174         assert_eq!(invalid_json["inner_code"], "event_json");
   2175 
   2176         let mut event: serde_json::Value =
   2177             serde_json::from_str(&signed_claim_event_json()).expect("event json");
   2178         event["sig"] = serde_json::Value::String("0".repeat(128));
   2179         let signature_error =
   2180             verify_and_decode_event_json(&event.to_string()).expect_err("signature error");
   2181         let signature_error: serde_json::Value =
   2182             serde_json::from_str(&signature_error).expect("signature error json");
   2183         assert_eq!(signature_error["code"], "nip01_verification");
   2184         assert_eq!(signature_error["inner_code"], "signature_invalid");
   2185     }
   2186 
   2187     #[test]
   2188     fn knowledge_bindings_reject_unchecked_event_envelope_input() {
   2189         let mut oversized: serde_json::Value =
   2190             serde_json::from_str(&signed_claim_event_json()).expect("event json");
   2191         oversized["content"] = serde_json::Value::String(
   2192             "x".repeat(EventEnvelopeLimits::default().max_content_bytes + 1),
   2193         );
   2194         let oversized_error =
   2195             verify_and_decode_event_json(&oversized.to_string()).expect_err("oversized content");
   2196         let oversized_error: serde_json::Value =
   2197             serde_json::from_str(&oversized_error).expect("oversized error json");
   2198         assert_eq!(oversized_error["code"], "invalid_event");
   2199         assert_eq!(oversized_error["inner_code"], "content_too_large");
   2200 
   2201         let mut non_canonical: serde_json::Value =
   2202             serde_json::from_str(&signed_claim_event_json()).expect("event json");
   2203         non_canonical["id"] = serde_json::Value::String("A".repeat(64));
   2204         let canonical_error =
   2205             verify_and_decode_event_json(&non_canonical.to_string()).expect_err("non-canonical id");
   2206         let canonical_error: serde_json::Value =
   2207             serde_json::from_str(&canonical_error).expect("canonical error json");
   2208         assert_eq!(canonical_error["code"], "invalid_event");
   2209         assert_eq!(canonical_error["inner_code"], "id_non_canonical");
   2210 
   2211         let mut unknown_field: serde_json::Value =
   2212             serde_json::from_str(&signed_claim_event_json()).expect("event json");
   2213         unknown_field["verified"] = serde_json::Value::Bool(true);
   2214         let unknown_error = verify_and_decode_event_json(&unknown_field.to_string())
   2215             .expect_err("unknown event field");
   2216         let unknown_error: serde_json::Value =
   2217             serde_json::from_str(&unknown_error).expect("unknown-field error json");
   2218         assert_eq!(unknown_error["code"], "invalid_json");
   2219         assert_eq!(unknown_error["inner_code"], "event_json");
   2220     }
   2221 
   2222     #[test]
   2223     fn event_envelope_error_codes_cover_every_structural_limit_class() {
   2224         let cases = [
   2225             (
   2226                 EventEnvelopeError::NonCanonicalAuthor,
   2227                 "author_non_canonical",
   2228             ),
   2229             (
   2230                 EventEnvelopeError::NonCanonicalSignature,
   2231                 "signature_non_canonical",
   2232             ),
   2233             (EventEnvelopeError::EmptyTag { index: 0 }, "tag_empty"),
   2234             (
   2235                 EventEnvelopeError::EmptyTagKey { index: 0 },
   2236                 "tag_key_empty",
   2237             ),
   2238             (
   2239                 EventEnvelopeError::ControlCharacterTagKey { index: 0 },
   2240                 "tag_key_control_character",
   2241             ),
   2242             (
   2243                 EventEnvelopeError::TooManyTags { max: 1, actual: 2 },
   2244                 "too_many_tags",
   2245             ),
   2246             (
   2247                 EventEnvelopeError::TooManyTagElements { max: 1, actual: 2 },
   2248                 "too_many_tag_elements",
   2249             ),
   2250             (
   2251                 EventEnvelopeError::TagElementTooLarge {
   2252                     tag_index: 0,
   2253                     element_index: 0,
   2254                     max: 1,
   2255                     actual: 2,
   2256                 },
   2257                 "tag_element_too_large",
   2258             ),
   2259             (
   2260                 EventEnvelopeError::TagsTooLarge { max: 1, actual: 2 },
   2261                 "tags_too_large",
   2262             ),
   2263         ];
   2264         for (error, code) in cases {
   2265             assert_eq!(envelope_error_code(&error), code);
   2266         }
   2267     }
   2268 
   2269     #[test]
   2270     fn field_bindings_encode_to_json_when_input_is_valid() {
   2271         let workspace_json =
   2272             serde_json::to_string(&sample_workspace_manifest()).expect("workspace json");
   2273         assert_tags_json(farm_workspace_manifest_tags(&workspace_json));
   2274 
   2275         let crdt_json = serde_json::json!({
   2276             "change": sample_crdt_change(),
   2277             "author_pubkey": "author_pubkey"
   2278         })
   2279         .to_string();
   2280         assert_tags_json(farm_crdt_change_tags(&crdt_json));
   2281 
   2282         let file_json = serde_json::to_string(&sample_file_metadata()).expect("file json");
   2283         assert_tags_json(farm_file_metadata_tags(&file_json));
   2284 
   2285         let relay_auth_json = serde_json::to_string(&RelayAuth {
   2286             relay: "wss://relay.example.invalid/farm/field-group".to_string(),
   2287             challenge: "relay-provided-challenge".to_string(),
   2288         })
   2289         .expect("relay auth json");
   2290         assert_tags_json(relay_auth_tags(&relay_auth_json));
   2291 
   2292         let http_auth_json = serde_json::to_string(&HttpAuth {
   2293             url: "https://media.example.invalid/upload".to_string(),
   2294             method: "POST".to_string(),
   2295             payload_sha256: Some(
   2296                 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
   2297             ),
   2298         })
   2299         .expect("http auth json");
   2300         assert_tags_json(http_auth_tags(&http_auth_json));
   2301     }
   2302 
   2303     #[test]
   2304     fn field_bindings_surface_builder_errors() {
   2305         let crdt_json = serde_json::json!({
   2306             "change": sample_crdt_change(),
   2307             "author_pubkey": " "
   2308         })
   2309         .to_string();
   2310 
   2311         assert!(farm_crdt_change_tags(&crdt_json).is_err());
   2312     }
   2313 
   2314     #[test]
   2315     fn group_bindings_encode_to_json_when_input_is_valid() {
   2316         let metadata = sample_group_metadata();
   2317         assert_tags_json(group_put_user_tags(
   2318             &serde_json::to_string(&GroupPutUser {
   2319                 group_id: "field-group".to_string(),
   2320                 message: Some("add member".to_string()),
   2321                 pubkey: "member_pubkey".to_string(),
   2322                 roles: vec!["member".to_string()],
   2323             })
   2324             .expect("put user json"),
   2325         ));
   2326         assert_tags_json(group_remove_user_tags(
   2327             &serde_json::to_string(&GroupRemoveUser {
   2328                 group_id: "field-group".to_string(),
   2329                 message: Some("remove member".to_string()),
   2330                 pubkey: "member_pubkey".to_string(),
   2331             })
   2332             .expect("remove user json"),
   2333         ));
   2334         assert_tags_json(group_create_group_tags(
   2335             &serde_json::to_string(&GroupCreateGroup {
   2336                 group_id: "field-group".to_string(),
   2337                 message: Some("create group".to_string()),
   2338                 metadata: metadata.clone(),
   2339             })
   2340             .expect("create group json"),
   2341         ));
   2342         assert_tags_json(group_edit_metadata_tags(
   2343             &serde_json::to_string(&GroupEditMetadata {
   2344                 group_id: "field-group".to_string(),
   2345                 message: Some("edit metadata".to_string()),
   2346                 metadata: metadata.clone(),
   2347             })
   2348             .expect("edit metadata json"),
   2349         ));
   2350         assert_tags_json(group_delete_group_tags(
   2351             &serde_json::to_string(&GroupDeleteGroup {
   2352                 group_id: "field-group".to_string(),
   2353                 message: Some("delete group".to_string()),
   2354             })
   2355             .expect("delete group json"),
   2356         ));
   2357         assert_tags_json(group_delete_event_tags(
   2358             &serde_json::to_string(&GroupDeleteEvent {
   2359                 group_id: "field-group".to_string(),
   2360                 message: Some("delete event".to_string()),
   2361                 event_id: "event_id".to_string(),
   2362             })
   2363             .expect("delete event json"),
   2364         ));
   2365         let invite_tags = tags_json(group_create_invite_tags(
   2366             &serde_json::to_string(&GroupCreateInvite {
   2367                 group_id: "field-group".to_string(),
   2368                 message: Some("join the field group".to_string()),
   2369                 code: "invite-code".to_string(),
   2370             })
   2371             .expect("invite json"),
   2372         ));
   2373         assert!(invite_tags.contains(&vec!["code".to_string(), "invite-code".to_string()]));
   2374         assert_tags_json(group_join_request_tags(
   2375             &serde_json::to_string(&GroupJoinRequest {
   2376                 group_id: "field-group".to_string(),
   2377                 message: Some("requesting access".to_string()),
   2378                 code: Some("invite-code".to_string()),
   2379             })
   2380             .expect("join json"),
   2381         ));
   2382         assert_tags_json(group_leave_request_tags(
   2383             &serde_json::to_string(&GroupLeaveRequest {
   2384                 group_id: "field-group".to_string(),
   2385                 message: Some("leaving".to_string()),
   2386             })
   2387             .expect("leave json"),
   2388         ));
   2389         let metadata_tags = tags_json(group_metadata_tags(
   2390             &serde_json::to_string(&GroupMetadata {
   2391                 d_tag: "field-group".to_string(),
   2392                 metadata,
   2393             })
   2394             .expect("metadata json"),
   2395         ));
   2396         assert!(metadata_tags.contains(&vec!["restricted".to_string()]));
   2397         assert!(metadata_tags.contains(&vec![
   2398             "supported_kinds".to_string(),
   2399             "78".to_string(),
   2400             "30078".to_string(),
   2401             KIND_FARM_FILE_METADATA.to_string()
   2402         ]));
   2403         assert_tags_json(group_admins_tags(
   2404             &serde_json::to_string(&GroupAdmins {
   2405                 d_tag: "field-group".to_string(),
   2406                 description: Some("group admins".to_string()),
   2407                 admins: vec![sample_group_user("admin")],
   2408             })
   2409             .expect("admins json"),
   2410         ));
   2411         assert_tags_json(group_members_tags(
   2412             &serde_json::to_string(&GroupMembers {
   2413                 d_tag: "field-group".to_string(),
   2414                 description: Some("group members".to_string()),
   2415                 members: vec![sample_group_user("member")],
   2416             })
   2417             .expect("members json"),
   2418         ));
   2419         assert_tags_json(group_roles_tags(
   2420             &serde_json::to_string(&GroupRoles {
   2421                 d_tag: "field-group".to_string(),
   2422                 description: Some("group roles".to_string()),
   2423                 roles: vec![sample_group_role()],
   2424             })
   2425             .expect("roles json"),
   2426         ));
   2427     }
   2428 
   2429     #[test]
   2430     fn listing_bindings_surface_builder_errors() {
   2431         let mut listing_json = serde_json::to_value(sample_listing()).expect("listing value");
   2432         listing_json["bins"] = serde_json::Value::Array(Vec::new());
   2433         let listing_json = serde_json::to_string(&listing_json).expect("listing json");
   2434 
   2435         assert!(operational_listing_tags(&listing_json).is_err());
   2436         assert!(operational_listing_tags_full(&listing_json).is_err());
   2437     }
   2438 
   2439     #[test]
   2440     fn evidence_report_plan_and_signed_validation_use_final_contracts() {
   2441         assert!(trade_evidence_manifest_parse_json("AA").is_err());
   2442         assert!(
   2443             rhi_evidence_attestation_validate_signed_json(
   2444                 &"x".repeat(radroots_event::wire::v1::DEFAULT_RAW_JSON_MAX_BYTES + 1)
   2445             )
   2446             .is_err()
   2447         );
   2448 
   2449         let fixture: serde_json::Value = serde_json::from_str(include_str!(
   2450             "../../../contracts/conformance/vectors/event/authored_operations.v1.json"
   2451         ))
   2452         .expect("authored corpus");
   2453         let expected = fixture["vectors"]
   2454             .as_array()
   2455             .expect("operations")
   2456             .iter()
   2457             .find(|entry| entry["id"] == "typed_rhi_evidence_attestation_017")
   2458             .expect("RHI operation")
   2459             .get("expected")
   2460             .expect("expected");
   2461         let content = expected["content"].as_str().expect("content");
   2462 
   2463         let report: serde_json::Value = serde_json::from_str(
   2464             &rhi_evidence_report_parse_json(content).expect("report projection"),
   2465         )
   2466         .expect("report JSON");
   2467         assert_eq!(report["outcome"], "indeterminate");
   2468         assert_eq!(report["trade_generation"], "7");
   2469 
   2470         let plan: serde_json::Value = serde_json::from_str(
   2471             &rhi_evidence_attestation_build_draft(content, 1_784_347_200).expect("typed plan"),
   2472         )
   2473         .expect("plan JSON");
   2474         assert_eq!(plan["kind"], 3_441);
   2475         assert_eq!(plan["created_at_unix_s"], "1784347200");
   2476         assert_eq!(plan["expected_event_id"], expected["event_id"]);
   2477 
   2478         let raw: serde_json::Value =
   2479             serde_json::from_str(expected["raw_json"].as_str().expect("signed event"))
   2480                 .expect("raw event JSON");
   2481         let boundary_event = serde_json::json!({
   2482             "id": raw["id"],
   2483             "author": raw["pubkey"],
   2484             "created_at": raw["created_at"],
   2485             "kind": raw["kind"],
   2486             "tags": raw["tags"],
   2487             "content": raw["content"],
   2488             "sig": raw["sig"],
   2489         });
   2490         let attestation: serde_json::Value = serde_json::from_str(
   2491             &rhi_evidence_attestation_validate_signed_json(&boundary_event.to_string())
   2492                 .expect("verified attestation"),
   2493         )
   2494         .expect("attestation JSON");
   2495         assert_eq!(attestation["outcome"], "indeterminate");
   2496         assert_eq!(attestation["trade_generation"], "7");
   2497 
   2498         let secret = "private-report-content";
   2499         let error = rhi_evidence_report_parse_json(secret).expect_err("malformed report");
   2500         assert!(!format!("{error:?}").contains(secret));
   2501 
   2502         let decision_vectors: serde_json::Value = serde_json::from_str(include_str!(
   2503             "../../../contracts/conformance/vectors/rhi/evidence_attestation_decision.v1.json"
   2504         ))
   2505         .expect("RHI decision corpus");
   2506         let superseding = decision_vectors["vectors"]
   2507             .as_array()
   2508             .expect("RHI decision vectors")
   2509             .iter()
   2510             .find(|entry| entry["id"] == "rhi_evidence_attestation_superseding_002")
   2511             .expect("superseding vector");
   2512         let superseding_content = superseding["expected"]["canonical_event_content_utf8"]
   2513             .as_str()
   2514             .expect("superseding canonical content");
   2515         let superseding_report: serde_json::Value = serde_json::from_str(
   2516             &rhi_evidence_report_parse_json(superseding_content)
   2517                 .expect("superseding report projection"),
   2518         )
   2519         .expect("superseding report JSON");
   2520         assert_eq!(superseding_report["outcome"], "valid");
   2521         assert_eq!(
   2522             superseding_report["supersession"]["report_id"],
   2523             "7777777777777777777777777777777777777777777777777777777777777777"
   2524         );
   2525         assert_eq!(
   2526             superseding_report["supersession"]["event_id"],
   2527             "8888888888888888888888888888888888888888888888888888888888888888"
   2528         );
   2529     }
   2530 
   2531     #[test]
   2532     fn evidence_manifest_projection_covers_every_admitted_vocabulary() {
   2533         assert_eq!(
   2534             coverage_name(RadrootsTradeEvidenceCoverageV1::Missing),
   2535             "missing"
   2536         );
   2537         assert_eq!(
   2538             coverage_name(RadrootsTradeEvidenceCoverageV1::Partial),
   2539             "partial"
   2540         );
   2541         assert_eq!(
   2542             coverage_name(RadrootsTradeEvidenceCoverageV1::ScopeSatisfied),
   2543             "scope_satisfied"
   2544         );
   2545         assert_eq!(
   2546             coverage_name(RadrootsTradeEvidenceCoverageV1::Unsupported),
   2547             "unsupported"
   2548         );
   2549         assert_eq!(outcome_name(RadrootsTradeEvidenceOutcomeV1::Valid), "valid");
   2550         assert_eq!(
   2551             outcome_name(RadrootsTradeEvidenceOutcomeV1::Invalid),
   2552             "invalid"
   2553         );
   2554         assert_eq!(
   2555             outcome_name(RadrootsTradeEvidenceOutcomeV1::Indeterminate),
   2556             "indeterminate"
   2557         );
   2558         assert_eq!(
   2559             attestation_outcome_name(RadrootsRhiEvidenceAttestationOutcomeV1::Valid),
   2560             "valid"
   2561         );
   2562         assert_eq!(
   2563             attestation_outcome_name(RadrootsRhiEvidenceAttestationOutcomeV1::Invalid),
   2564             "invalid"
   2565         );
   2566         assert_eq!(
   2567             attestation_outcome_name(RadrootsRhiEvidenceAttestationOutcomeV1::Indeterminate),
   2568             "indeterminate"
   2569         );
   2570 
   2571         let source = RadrootsTradeEvidenceManifestSourceResultV1::new(
   2572             RadrootsTradeEvidenceSourceIdV1::parse("typed_source").expect("source id"),
   2573             RadrootsTradeEvidenceSourceResultV1::new(
   2574                 RadrootsTradeEvidenceSourceRequirementV1::Required,
   2575                 RadrootsTradeEvidenceSourceCompletionV1::Complete,
   2576                 0,
   2577             )
   2578             .expect("source result"),
   2579             RadrootsTradeEvidenceSourceResultDigestV1::from_bytes([0x33; 32]),
   2580         );
   2581         let manifest = RadrootsTradeEvidenceManifestV1::new(
   2582             TradeId::from_bytes([0x11; 16]),
   2583             NonZeroU64::new(1).expect("nonzero generation"),
   2584             RadrootsTradeEvidencePolicyDigestV1::from_bytes([0x22; 32]),
   2585             1_800_000_000,
   2586             RadrootsTradeEvidenceScopePrerequisitesV1::Satisfied,
   2587             [source],
   2588             [],
   2589         )
   2590         .expect("manifest");
   2591         let canonical_hex = hex::encode(manifest.canonical_bytes());
   2592         let projected: serde_json::Value = serde_json::from_str(
   2593             &trade_evidence_manifest_parse_json(&canonical_hex).expect("manifest projection"),
   2594         )
   2595         .expect("manifest projection JSON");
   2596         assert_eq!(projected["coverage"], "scope_satisfied");
   2597         assert_eq!(projected["canonical_bytes_hex"], canonical_hex);
   2598 
   2599         assert!(trade_evidence_manifest_parse_json("0").is_err());
   2600         assert!(
   2601             trade_evidence_manifest_parse_json(&"0".repeat(
   2602                 radroots_trade::evidence::RADROOTS_TRADE_EVIDENCE_MANIFEST_MAXIMUM_BYTES * 2 + 2
   2603             ))
   2604             .is_err()
   2605         );
   2606     }
   2607 }