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 }