CLI app for developers prototyping atproto functionality
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Refactor to clean up code

authored by

Jack Grigg and committed by
Tangled
638235ee 5286165b

+307 -327
+262 -320
src/commands/test/labeler/create_report.rs
··· 5 5 //! in Phase 4. The `sentinel` submodule is self-contained and is exercised 6 6 //! by later phases for the pollution-avoidance sentinel reason string. 7 7 8 - use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode}; 9 8 use std::borrow::Cow; 10 - use std::net::SocketAddr; 11 9 use std::sync::Arc; 12 10 use std::time::{Duration, SystemTime, UNIX_EPOCH}; 13 11 use url::Url; ··· 27 25 pub mod self_mint; 28 26 pub mod sentinel; 29 27 30 - /// Construct `did:web:127.0.0.1%3A{port}` for a self-mint identity bound 31 - /// to the given local `SocketAddr`. The `:` between the IP and the port is 32 - /// percent-encoded per atproto did:web rules. 33 - /// 34 - /// Uses the `SocketAddr` IP literally (typically `127.0.0.1`). IPv6 35 - /// loopback would produce `did:web:::1%3A{port}` which the atproto did 36 - /// syntax regex rejects; for v1 the self-mint server is IPv4-only. 37 - pub(crate) fn self_mint_did_for(addr: SocketAddr) -> Did { 38 - assert!(addr.is_ipv4(), "self-mint DidDocServer is IPv4-only"); 39 - let host = addr.ip().to_string(); 40 - let port = addr.port(); 41 - // Percent-encode the `:` (and, defensively, any other non-alphanumeric) 42 - // with the standard set. For the `127.0.0.1:{port}` case this yields 43 - // exactly `127.0.0.1%3A{port}`. 44 - let encoded_hostport = format!("{host}{}{port}", utf8_percent_encode(":", NON_ALPHANUMERIC)); 45 - Did(format!("did:web:{encoded_hostport}")) 46 - } 47 - 48 - /// Base URL the labeler uses to fetch the self-mint DID document: 49 - /// `http://127.0.0.1:{port}`. 50 - pub(crate) fn self_mint_base_url(addr: SocketAddr) -> Url { 51 - Url::parse(&format!("http://{addr}")).expect("SocketAddr Display is always a valid authority") 52 - } 53 - 54 28 /// Raw HTTP response from POSTing `com.atproto.moderation.createReport`. 55 29 /// 56 30 /// Mirrors `RawXrpcResponse` from the HTTP stage but specialized for the ··· 79 53 pub enum CreateReportStageError { 80 54 /// Transport-level failure: the request never reached a well-formed 81 55 /// HTTP exchange. 82 - #[error("createReport transport error: {message}")] 56 + #[error("createReport transport error: {source}")] 83 57 #[diagnostic(code = "labeler::report::transport_error")] 84 58 Transport { 85 - /// Human-readable error message. 86 - message: String, 87 - /// Underlying reqwest error, if available. 59 + /// Underlying error. 88 60 #[source] 89 - source: Option<Box<dyn std::error::Error + Send + Sync>>, 61 + source: Box<dyn std::error::Error + Send + Sync>, 90 62 }, 91 63 } 92 64 ··· 200 172 .send() 201 173 .await 202 174 .map_err(|e| CreateReportStageError::Transport { 203 - message: e.to_string(), 204 - source: Some(Box::new(e)), 175 + source: Box::new(e), 205 176 })?; 206 177 let status = resp.status(); 207 178 let content_type = resp ··· 213 184 .bytes() 214 185 .await 215 186 .map_err(|e| CreateReportStageError::Transport { 216 - message: e.to_string(), 217 - source: Some(Box::new(e)), 187 + source: Box::new(e), 218 188 })?; 219 189 Ok(RawPdsXrpcResponse { 220 190 status, ··· 247 217 .send() 248 218 .await 249 219 .map_err(|e| CreateReportStageError::Transport { 250 - message: e.to_string(), 251 - source: Some(Box::new(e)), 220 + source: Box::new(e), 252 221 })?; 253 222 let status = resp.status(); 254 223 let content_type = resp ··· 260 229 .bytes() 261 230 .await 262 231 .map_err(|e| CreateReportStageError::Transport { 263 - message: e.to_string(), 264 - source: Some(Box::new(e)), 232 + source: Box::new(e), 265 233 })?; 266 234 Ok(RawPdsXrpcResponse { 267 235 status, ··· 322 290 .send() 323 291 .await 324 292 .map_err(|e| CreateReportStageError::Transport { 325 - message: e.to_string(), 326 - source: Some(Box::new(e)), 293 + source: Box::new(e), 327 294 })?; 328 295 329 296 let status = response.status(); ··· 337 304 .bytes() 338 305 .await 339 306 .map_err(|e| CreateReportStageError::Transport { 340 - message: e.to_string(), 341 - source: Some(Box::new(e)), 307 + source: Box::new(e), 342 308 })?; 343 309 344 310 tracing::debug!( ··· 363 329 /// a `NetworkError` by the report stage (per AC5.3 / AC6.3). 364 330 #[derive(Debug)] 365 331 pub enum PdsJwtFetchError { 366 - /// Any failure at the PDS boundary — unreachable, auth rejected, etc. 367 - Pds { message: String }, 332 + Transport(CreateReportStageError), 333 + Failed(RawPdsXrpcResponse), 334 + InvalidBody { 335 + resp: RawPdsXrpcResponse, 336 + error: serde_json::Error, 337 + }, 338 + MissingToken(RawPdsXrpcResponse), 368 339 } 369 340 370 341 impl std::fmt::Display for PdsJwtFetchError { 371 342 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 372 343 match self { 373 - PdsJwtFetchError::Pds { message } => write!(f, "{message}"), 344 + PdsJwtFetchError::Transport(e) => write!(f, "getServiceAuth transport: {e}"), 345 + PdsJwtFetchError::Failed(resp) => { 346 + write!(f, "getServiceAuth returned status {}", resp.status) 347 + } 348 + PdsJwtFetchError::InvalidBody { error, .. } => { 349 + write!(f, "getServiceAuth body not JSON: {error}") 350 + } 351 + PdsJwtFetchError::MissingToken(_) => write!(f, "getServiceAuth response missing token"), 374 352 } 375 353 } 376 354 } ··· 407 385 &[("aud", aud), ("lxm", lxm), ("exp", &exp_s)], 408 386 ) 409 387 .await 410 - .map_err(|e| PdsJwtFetchError::Pds { 411 - message: format!("getServiceAuth transport: {e}"), 412 - })?; 388 + .map_err(PdsJwtFetchError::Transport)?; 413 389 if !resp.status.is_success() { 414 - return Err(PdsJwtFetchError::Pds { 415 - message: format!("getServiceAuth returned status {}", resp.status), 416 - }); 390 + return Err(PdsJwtFetchError::Failed(resp)); 417 391 } 418 - let auth: serde_json::Value = 419 - serde_json::from_slice(&resp.raw_body).map_err(|e| PdsJwtFetchError::Pds { 420 - message: format!("getServiceAuth body not JSON: {e}"), 421 - })?; 422 - let token = auth["token"] 423 - .as_str() 424 - .ok_or_else(|| PdsJwtFetchError::Pds { 425 - message: "getServiceAuth response missing token".to_string(), 426 - })? 427 - .to_string(); 392 + let token = match serde_json::from_slice::<serde_json::Value>(&resp.raw_body) { 393 + Err(error) => Err(PdsJwtFetchError::InvalidBody { resp, error }), 394 + Ok(auth) => auth["token"] 395 + .as_str() 396 + .map(|s| s.to_string()) 397 + .ok_or_else(|| PdsJwtFetchError::MissingToken(resp)), 398 + }?; 428 399 429 400 Ok(token) 430 401 } ··· 549 520 550 521 /// Build a `SpecViolation` result for this check with an optional 551 522 /// diagnostic. 552 - pub fn spec_violation( 553 - self, 554 - diagnostic: Option<Box<dyn miette::Diagnostic + Send + Sync>>, 555 - ) -> CheckResult { 523 + pub fn spec_violation(self, diagnostic: CreateReportDiagnostic) -> CheckResult { 556 524 CheckResult { 557 525 id: self.id(), 558 526 stage: Stage::Report, 559 527 status: CheckStatus::SpecViolation, 560 528 summary: Cow::Borrowed(self.default_summary_fail()), 561 - diagnostic, 529 + diagnostic: Some(Box::new(diagnostic) as _), 562 530 skipped_reason: None, 563 531 } 564 532 } 565 533 566 534 /// Build an `Advisory` result (used by `rejected_shape_returns_400` AC3.6). 567 - pub fn advisory( 568 - self, 569 - diagnostic: Option<Box<dyn miette::Diagnostic + Send + Sync>>, 570 - ) -> CheckResult { 535 + pub fn advisory(self, diagnostic: CreateReportDiagnostic) -> CheckResult { 571 536 CheckResult { 572 537 id: self.id(), 573 538 stage: Stage::Report, 574 539 status: CheckStatus::Advisory, 575 540 summary: Cow::Borrowed(self.default_summary_fail()), 576 - diagnostic, 541 + diagnostic: Some(Box::new(diagnostic) as _), 577 542 skipped_reason: None, 578 543 } 579 544 } ··· 637 602 } 638 603 } 639 604 640 - /// Diagnostic for the `contract_missing` spec violation (AC1.3). 641 - /// 642 - /// Emitted when `--commit-report` is set and the identity-stage 643 - /// `labeler_policies` does not advertise a non-empty `reasonTypes` and 644 - /// `subjectTypes`. The body of the labeler record is attached as source 645 - /// so users can see what _was_ published. 646 - #[derive(Debug, Error, Diagnostic)] 647 - #[error("Labeler does not advertise a reportable `LabelerPolicies` shape")] 648 - #[diagnostic( 649 - code = "labeler::report::contract_missing", 650 - help = "`reasonTypes` and `subjectTypes` must both be present and non-empty on the labeler's published policies; the tool cannot verify reporting conformance without them." 651 - )] 652 - pub struct ContractMissing { 653 - /// `reasonTypes` present and non-empty? 654 - pub has_reason_types: bool, 655 - /// `subjectTypes` present and non-empty? 656 - pub has_subject_types: bool, 657 - } 658 - 659 605 /// Aggregate of the stage-relevant options, extracted from `LabelerOptions` 660 606 /// by the pipeline and passed to `run`. Having a local, narrow shape 661 607 /// avoids forcing `run`'s signature to take everything in `LabelerOptions`. ··· 713 659 if opts.commit_report { 714 660 // AC1.3: commit requested, contract missing ⇒ SpecViolation + 715 661 // every other check blocked by this one. 716 - let diag = Box::new(ContractMissing { 662 + let diag = CreateReportDiagnostic::ContractMissing { 717 663 has_reason_types, 718 664 has_subject_types, 719 - }); 720 - results.push(Check::ContractPublished.spec_violation(Some(diag))); 665 + }; 666 + results.push(Check::ContractPublished.spec_violation(diag)); 721 667 for c in Check::ORDER.iter().skip(1).copied() { 722 668 results.push(c.skip("blocked by `report::contract_published`")); 723 669 } ··· 763 709 RejectionShape::WrongStatus { status } => { 764 710 let status_u16 = status.as_u16(); 765 711 let (source_code, span) = body_as_named_source(&resp); 766 - let diag = Box::new(UnauthenticatedAccepted { 712 + let diag = CreateReportDiagnostic::UnauthenticatedAccepted { 767 713 status: status_u16, 768 714 source_code, 769 715 span, 770 - }); 771 - results.push(Check::UnauthenticatedRejected.spec_violation(Some(diag))); 716 + }; 717 + results.push(Check::UnauthenticatedRejected.spec_violation(diag)); 772 718 } 773 719 }, 774 - Err(CreateReportStageError::Transport { message, .. }) => { 775 - results.push(Check::UnauthenticatedRejected.network_error(message)); 720 + Err(CreateReportStageError::Transport { source }) => { 721 + results.push(Check::UnauthenticatedRejected.network_error(source.to_string())); 776 722 } 777 723 } 778 724 ··· 796 742 RejectionShape::WrongStatus { status } => { 797 743 let status_u16 = status.as_u16(); 798 744 let (source_code, span) = body_as_named_source(&resp); 799 - let diag = Box::new(MalformedBearerAccepted { 745 + let diag = CreateReportDiagnostic::MalformedBearerAccepted { 800 746 status: status_u16, 801 747 source_code, 802 748 span, 803 - }); 804 - results.push(Check::MalformedBearerRejected.spec_violation(Some(diag))); 749 + }; 750 + results.push(Check::MalformedBearerRejected.spec_violation(diag)); 805 751 } 806 752 }, 807 - Err(CreateReportStageError::Transport { message, .. }) => { 808 - results.push(Check::MalformedBearerRejected.network_error(message)); 753 + Err(CreateReportStageError::Transport { source }) => { 754 + results.push(Check::MalformedBearerRejected.network_error(source.to_string())); 809 755 } 810 756 } 811 757 ··· 867 813 } 868 814 RejectionShape::WrongStatus { .. } => { 869 815 let (source_code, span) = body_as_named_source(&resp); 870 - let diag = Box::new(WrongAudAccepted { 816 + let diag = CreateReportDiagnostic::WrongAudAccepted { 871 817 status: resp.status.as_u16(), 872 818 source_code, 873 819 span, 874 - }); 875 - results.push(Check::WrongAudRejected.spec_violation(Some(diag))); 820 + }; 821 + results.push(Check::WrongAudRejected.spec_violation(diag)); 876 822 } 877 823 }, 878 - Err(CreateReportStageError::Transport { message, .. }) => { 879 - results.push(Check::WrongAudRejected.network_error(message)); 824 + Err(CreateReportStageError::Transport { source }) => { 825 + results.push(Check::WrongAudRejected.network_error(source.to_string())); 880 826 } 881 827 } 882 828 } ··· 904 850 } 905 851 RejectionShape::WrongStatus { .. } => { 906 852 let (source_code, span) = body_as_named_source(&resp); 907 - let diag = Box::new(WrongLxmAccepted { 853 + let diag = CreateReportDiagnostic::WrongLxmAccepted { 908 854 status: resp.status.as_u16(), 909 855 source_code, 910 856 span, 911 - }); 912 - results.push(Check::WrongLxmRejected.spec_violation(Some(diag))); 857 + }; 858 + results.push(Check::WrongLxmRejected.spec_violation(diag)); 913 859 } 914 860 }, 915 - Err(CreateReportStageError::Transport { message, .. }) => { 916 - results.push(Check::WrongLxmRejected.network_error(message)); 861 + Err(CreateReportStageError::Transport { source }) => { 862 + results.push(Check::WrongLxmRejected.network_error(source.to_string())); 917 863 } 918 864 } 919 865 } ··· 942 888 } 943 889 RejectionShape::WrongStatus { .. } => { 944 890 let (source_code, span) = body_as_named_source(&resp); 945 - let diag = Box::new(ExpiredAccepted { 891 + let diag = CreateReportDiagnostic::ExpiredAccepted { 946 892 status: resp.status.as_u16(), 947 893 source_code, 948 894 span, 949 - }); 950 - results.push(Check::ExpiredRejected.spec_violation(Some(diag))); 895 + }; 896 + results.push(Check::ExpiredRejected.spec_violation(diag)); 951 897 } 952 898 }, 953 - Err(CreateReportStageError::Transport { message, .. }) => { 954 - results.push(Check::ExpiredRejected.network_error(message)); 899 + Err(CreateReportStageError::Transport { source }) => { 900 + results.push(Check::ExpiredRejected.network_error(source.to_string())); 955 901 } 956 902 } 957 903 } ··· 989 935 { 990 936 // AC3.6: 401 or 5xx → Advisory with shape_not_400. 991 937 let (source_code, span) = body_as_named_source(&resp); 992 - let diag = Box::new(ShapeNot400 { 938 + let diag = CreateReportDiagnostic::ShapeNot400 { 993 939 status: resp.status.as_u16(), 994 940 error_name: error_name.clone(), 995 941 source_code, 996 942 span, 997 - }); 998 - results.push(Check::RejectedShapeReturns400.advisory(Some(diag))); 943 + }; 944 + results.push(Check::RejectedShapeReturns400.advisory(diag)); 999 945 } else if resp.status == reqwest::StatusCode::BAD_REQUEST { 1000 946 // 400 but not `InvalidRequest` name → Advisory. 1001 947 let (source_code, span) = body_as_named_source(&resp); 1002 - let diag = Box::new(ShapeNot400 { 948 + let diag = CreateReportDiagnostic::ShapeNot400 { 1003 949 status: 400, 1004 950 error_name: error_name.clone(), 1005 951 source_code, 1006 952 span, 1007 - }); 1008 - results.push(Check::RejectedShapeReturns400.advisory(Some(diag))); 953 + }; 954 + results.push(Check::RejectedShapeReturns400.advisory(diag)); 1009 955 } else { 1010 956 // Catch-all: 200 accepted → Advisory. A 200 for an invalid 1011 957 // shape is a labeler looseness issue, not the same category 1012 958 // as the `self_mint_accepted` SpecViolation (which expects 1013 959 // a *valid* shape to be accepted). 1014 960 let (source_code, span) = body_as_named_source(&resp); 1015 - let diag = Box::new(ShapeNot400 { 961 + let diag = CreateReportDiagnostic::ShapeNot400 { 1016 962 status: resp.status.as_u16(), 1017 963 error_name, 1018 964 source_code, 1019 965 span, 1020 - }); 1021 - results.push(Check::RejectedShapeReturns400.advisory(Some(diag))); 966 + }; 967 + results.push(Check::RejectedShapeReturns400.advisory(diag)); 1022 968 } 1023 969 } 1024 - Err(CreateReportStageError::Transport { message, .. }) => { 1025 - results.push(Check::RejectedShapeReturns400.network_error(message)); 970 + Err(CreateReportStageError::Transport { source }) => { 971 + results.push(Check::RejectedShapeReturns400.network_error(source.to_string())); 1026 972 } 1027 973 } 1028 974 } ··· 1107 1053 Ok(resp) => { 1108 1054 // AC4.3: non-2xx ⇒ SpecViolation. 1109 1055 let (source_code, span) = body_as_named_source(&resp); 1110 - let diag = Box::new(SelfMintRejected { 1056 + let diag = CreateReportDiagnostic::SelfMintRejected { 1111 1057 status: resp.status.as_u16(), 1112 1058 source_code, 1113 1059 span, 1114 - }); 1115 - results.push(Check::SelfMintAccepted.spec_violation(Some(diag))); 1060 + }; 1061 + results.push(Check::SelfMintAccepted.spec_violation(diag)); 1116 1062 } 1117 - Err(CreateReportStageError::Transport { message, .. }) => { 1118 - results.push(Check::SelfMintAccepted.network_error(message)); 1063 + Err(CreateReportStageError::Transport { source }) => { 1064 + results.push(Check::SelfMintAccepted.network_error(source.to_string())); 1119 1065 } 1120 1066 } 1121 1067 } else { ··· 1182 1128 ) 1183 1129 .await 1184 1130 { 1185 - Err(PdsJwtFetchError::Pds { message }) => { 1186 - results.push(Check::PdsServiceAuthAccepted.network_error(message)); 1131 + Err(e) => { 1132 + results.push(Check::PdsServiceAuthAccepted.network_error(e.to_string())); 1187 1133 } 1188 1134 Ok(service_jwt) => { 1189 1135 match report_tee ··· 1195 1141 } 1196 1142 Ok(resp) => { 1197 1143 let (source_code, span) = body_as_named_source(&resp); 1198 - let diag = Box::new(PdsServiceAuthRejected { 1144 + let diag = CreateReportDiagnostic::PdsServiceAuthRejected { 1199 1145 status: resp.status.as_u16(), 1200 1146 source_code, 1201 1147 span, 1202 - }); 1203 - results 1204 - .push(Check::PdsServiceAuthAccepted.spec_violation(Some(diag))); 1148 + }; 1149 + results.push(Check::PdsServiceAuthAccepted.spec_violation(diag)); 1205 1150 } 1206 - Err(CreateReportStageError::Transport { message, .. }) => { 1151 + Err(CreateReportStageError::Transport { source }) => { 1207 1152 // Labeler-side transport failure during direct POST. 1208 - results.push(Check::PdsServiceAuthAccepted.network_error(message)); 1153 + results.push( 1154 + Check::PdsServiceAuthAccepted.network_error(source.to_string()), 1155 + ); 1209 1156 } 1210 1157 } 1211 1158 } ··· 1214 1161 // Mode 3: PDS-proxied. 1215 1162 let proxier = PdsProxiedPoster::new(pds_client); 1216 1163 match proxier.post(&id_facts.did.0, &access_jwt, &pds_body).await { 1217 - Err(CreateReportStageError::Transport { message, .. }) => { 1164 + Err(CreateReportStageError::Transport { source }) => { 1218 1165 // Transport to the PDS itself; classify PDS-side. 1219 - results.push(Check::PdsProxiedAccepted.network_error(message)); 1166 + results.push(Check::PdsProxiedAccepted.network_error(source.to_string())); 1220 1167 } 1221 1168 Ok(resp) if resp.status.is_success() => { 1222 1169 results.push(Check::PdsProxiedAccepted.pass()); ··· 1234 1181 if is_upstream_label_error { 1235 1182 // AC6.2: labeler-side rejection surfaced by PDS. 1236 1183 let (source_code, span) = body_as_named_source_from_pds(&resp); 1237 - let diag = Box::new(PdsProxiedRejected { 1184 + let diag = CreateReportDiagnostic::PdsProxiedRejected { 1238 1185 status: resp.status.as_u16(), 1239 1186 source_code, 1240 1187 span, 1241 - }); 1242 - results.push(Check::PdsProxiedAccepted.spec_violation(Some(diag))); 1188 + }; 1189 + results.push(Check::PdsProxiedAccepted.spec_violation(diag)); 1243 1190 } else { 1244 1191 // AC6.3: PDS-side rejection of the proxy attempt. 1245 1192 results.push(Check::PdsProxiedAccepted.network_error(format!( ··· 1326 1273 } 1327 1274 } 1328 1275 1329 - #[cfg(test)] 1330 - mod did_tests { 1331 - use super::*; 1332 - 1333 - #[test] 1334 - fn self_mint_did_encodes_colon() { 1335 - let addr: SocketAddr = "127.0.0.1:5000".parse().unwrap(); 1336 - let did = self_mint_did_for(addr); 1337 - assert_eq!(did.0, "did:web:127.0.0.1%3A5000"); 1338 - } 1339 - 1340 - #[test] 1341 - fn self_mint_base_url_uses_http() { 1342 - let addr: SocketAddr = "127.0.0.1:5000".parse().unwrap(); 1343 - let url = self_mint_base_url(addr); 1344 - assert_eq!(url.as_str(), "http://127.0.0.1:5000/"); 1345 - } 1346 - } 1347 - 1348 1276 /// A loosely-parsed atproto XRPC error envelope. Missing fields are 1349 1277 /// rendered as `None` rather than failing the parse — the "loose 1350 1278 /// assertion" philosophy in the design (see "Error envelope assertion ··· 1416 1344 } 1417 1345 } 1418 1346 1419 - /// Diagnostic for AC2.2: labeler accepted an unauthenticated createReport POST. 1420 1347 #[derive(Debug, Error, Diagnostic)] 1421 - #[error("Labeler accepted unauthenticated createReport (status {status})")] 1422 - #[diagnostic( 1423 - code = "labeler::report::unauthenticated_accepted", 1424 - help = "A labeler must reject createReport with 401 when no Authorization header is supplied." 1425 - )] 1426 - pub struct UnauthenticatedAccepted { 1427 - /// Observed status code, e.g., 200. 1428 - pub status: u16, 1429 - /// Response body for context. 1430 - #[source_code] 1431 - pub source_code: NamedSource<Arc<[u8]>>, 1432 - /// Span covering the response body so miette renders `source_code`. 1433 - #[label("accepted here")] 1434 - pub span: SourceSpan, 1435 - } 1348 + pub enum CreateReportDiagnostic { 1349 + /// Diagnostic for the `contract_missing` spec violation (AC1.3). 1350 + /// 1351 + /// Emitted when `--commit-report` is set and the identity-stage 1352 + /// `labeler_policies` does not advertise a non-empty `reasonTypes` and 1353 + /// `subjectTypes`. The body of the labeler record is attached as source 1354 + /// so users can see what _was_ published. 1355 + #[error("Labeler does not advertise a reportable `LabelerPolicies` shape")] 1356 + #[diagnostic( 1357 + code = "labeler::report::contract_missing", 1358 + help = "`reasonTypes` and `subjectTypes` must both be present and non-empty on the labeler's published policies; the tool cannot verify reporting conformance without them." 1359 + )] 1360 + ContractMissing { 1361 + /// `reasonTypes` present and non-empty? 1362 + has_reason_types: bool, 1363 + /// `subjectTypes` present and non-empty? 1364 + has_subject_types: bool, 1365 + }, 1436 1366 1437 - /// Diagnostic for AC2.4: labeler accepted a malformed bearer token. 1438 - #[derive(Debug, Error, Diagnostic)] 1439 - #[error("Labeler accepted malformed Bearer token (status {status})")] 1440 - #[diagnostic( 1441 - code = "labeler::report::malformed_bearer_accepted", 1442 - help = "A labeler must reject createReport with 401 when the Authorization header carries a non-JWT string." 1443 - )] 1444 - pub struct MalformedBearerAccepted { 1445 - /// Observed status code, e.g., 200. 1446 - pub status: u16, 1447 - /// Response body for context. 1448 - #[source_code] 1449 - pub source_code: NamedSource<Arc<[u8]>>, 1450 - /// Span covering the response body so miette renders `source_code`. 1451 - #[label("accepted here")] 1452 - pub span: SourceSpan, 1453 - } 1367 + /// Diagnostic for AC2.2: labeler accepted an unauthenticated createReport POST. 1368 + #[error("Labeler accepted unauthenticated createReport (status {status})")] 1369 + #[diagnostic( 1370 + code = "labeler::report::unauthenticated_accepted", 1371 + help = "A labeler must reject createReport with 401 when no Authorization header is supplied." 1372 + )] 1373 + UnauthenticatedAccepted { 1374 + /// Observed status code, e.g., 200. 1375 + status: u16, 1376 + /// Response body for context. 1377 + #[source_code] 1378 + source_code: NamedSource<Arc<[u8]>>, 1379 + /// Span covering the response body so miette renders `source_code`. 1380 + #[label("accepted here")] 1381 + span: SourceSpan, 1382 + }, 1454 1383 1455 - /// Diagnostic for AC3.2: labeler accepted JWT with wrong `aud` claim. 1456 - #[derive(Debug, Error, Diagnostic)] 1457 - #[error("Labeler accepted JWT with wrong `aud` (status {status})")] 1458 - #[diagnostic( 1459 - code = "labeler::report::wrong_aud_accepted", 1460 - help = "A labeler must reject JWTs whose `aud` claim does not match its own DID." 1461 - )] 1462 - pub struct WrongAudAccepted { 1463 - /// Observed status code, e.g., 200. 1464 - pub status: u16, 1465 - /// Response body for context. 1466 - #[source_code] 1467 - pub source_code: NamedSource<Arc<[u8]>>, 1468 - /// Span covering the response body so miette renders `source_code`. 1469 - #[label("accepted here")] 1470 - pub span: SourceSpan, 1471 - } 1384 + /// Diagnostic for AC2.4: labeler accepted a malformed bearer token. 1385 + #[error("Labeler accepted malformed Bearer token (status {status})")] 1386 + #[diagnostic( 1387 + code = "labeler::report::malformed_bearer_accepted", 1388 + help = "A labeler must reject createReport with 401 when the Authorization header carries a non-JWT string." 1389 + )] 1390 + MalformedBearerAccepted { 1391 + /// Observed status code, e.g., 200. 1392 + status: u16, 1393 + /// Response body for context. 1394 + #[source_code] 1395 + source_code: NamedSource<Arc<[u8]>>, 1396 + /// Span covering the response body so miette renders `source_code`. 1397 + #[label("accepted here")] 1398 + span: SourceSpan, 1399 + }, 1472 1400 1473 - /// Diagnostic for AC3.3: labeler accepted JWT with wrong `lxm` claim. 1474 - #[derive(Debug, Error, Diagnostic)] 1475 - #[error("Labeler accepted JWT with wrong `lxm` (status {status})")] 1476 - #[diagnostic( 1477 - code = "labeler::report::wrong_lxm_accepted", 1478 - help = "A labeler must reject JWTs whose `lxm` claim does not match the invoked Lexicon method." 1479 - )] 1480 - pub struct WrongLxmAccepted { 1481 - /// Observed status code, e.g., 200. 1482 - pub status: u16, 1483 - /// Response body for context. 1484 - #[source_code] 1485 - pub source_code: NamedSource<Arc<[u8]>>, 1486 - /// Span covering the response body so miette renders `source_code`. 1487 - #[label("accepted here")] 1488 - pub span: SourceSpan, 1489 - } 1401 + /// Diagnostic for AC3.2: labeler accepted JWT with wrong `aud` claim. 1402 + #[error("Labeler accepted JWT with wrong `aud` (status {status})")] 1403 + #[diagnostic( 1404 + code = "labeler::report::wrong_aud_accepted", 1405 + help = "A labeler must reject JWTs whose `aud` claim does not match its own DID." 1406 + )] 1407 + WrongAudAccepted { 1408 + /// Observed status code, e.g., 200. 1409 + status: u16, 1410 + /// Response body for context. 1411 + #[source_code] 1412 + source_code: NamedSource<Arc<[u8]>>, 1413 + /// Span covering the response body so miette renders `source_code`. 1414 + #[label("accepted here")] 1415 + span: SourceSpan, 1416 + }, 1417 + 1418 + /// Diagnostic for AC3.3: labeler accepted JWT with wrong `lxm` claim. 1419 + #[error("Labeler accepted JWT with wrong `lxm` (status {status})")] 1420 + #[diagnostic( 1421 + code = "labeler::report::wrong_lxm_accepted", 1422 + help = "A labeler must reject JWTs whose `lxm` claim does not match the invoked Lexicon method." 1423 + )] 1424 + WrongLxmAccepted { 1425 + /// Observed status code, e.g., 200. 1426 + status: u16, 1427 + /// Response body for context. 1428 + #[source_code] 1429 + source_code: NamedSource<Arc<[u8]>>, 1430 + /// Span covering the response body so miette renders `source_code`. 1431 + #[label("accepted here")] 1432 + span: SourceSpan, 1433 + }, 1490 1434 1491 - /// Diagnostic for AC3.4: labeler accepted expired JWT. 1492 - #[derive(Debug, Error, Diagnostic)] 1493 - #[error("Labeler accepted expired JWT (status {status})")] 1494 - #[diagnostic( 1495 - code = "labeler::report::expired_accepted", 1496 - help = "A labeler must reject JWTs whose `exp` claim is in the past." 1497 - )] 1498 - pub struct ExpiredAccepted { 1499 - /// Observed status code, e.g., 200. 1500 - pub status: u16, 1501 - /// Response body for context. 1502 - #[source_code] 1503 - pub source_code: NamedSource<Arc<[u8]>>, 1504 - /// Span covering the response body so miette renders `source_code`. 1505 - #[label("accepted here")] 1506 - pub span: SourceSpan, 1507 - } 1435 + /// Diagnostic for AC3.4: labeler accepted expired JWT. 1436 + #[error("Labeler accepted expired JWT (status {status})")] 1437 + #[diagnostic( 1438 + code = "labeler::report::expired_accepted", 1439 + help = "A labeler must reject JWTs whose `exp` claim is in the past." 1440 + )] 1441 + ExpiredAccepted { 1442 + /// Observed status code, e.g., 200. 1443 + status: u16, 1444 + /// Response body for context. 1445 + #[source_code] 1446 + source_code: NamedSource<Arc<[u8]>>, 1447 + /// Span covering the response body so miette renders `source_code`. 1448 + #[label("accepted here")] 1449 + span: SourceSpan, 1450 + }, 1508 1451 1509 - /// Diagnostic for AC3.6: labeler rejected invalid shape with wrong status. 1510 - #[derive(Debug, Error, Diagnostic)] 1511 - #[error("Unadvertised `reasonType` was rejected with status {status}, expected 400 InvalidRequest")] 1512 - #[diagnostic( 1513 - code = "labeler::report::shape_not_400", 1514 - help = "A labeler should return 400 InvalidRequest (not 401 or 500) for a `reasonType` not listed in its published LabelerPolicies.reasonTypes." 1515 - )] 1516 - pub struct ShapeNot400 { 1517 - /// Observed status code. 1518 - pub status: u16, 1519 - /// Error name from the response envelope, if present. 1520 - pub error_name: Option<String>, 1521 - /// Response body for context. 1522 - #[source_code] 1523 - pub source_code: NamedSource<Arc<[u8]>>, 1524 - /// Span covering the response body so miette renders `source_code`. 1525 - #[label("rejected with wrong status here")] 1526 - pub span: SourceSpan, 1527 - } 1452 + /// Diagnostic for AC3.6: labeler rejected invalid shape with wrong status. 1453 + #[error( 1454 + "Unadvertised `reasonType` was rejected with status {status}, expected 400 InvalidRequest" 1455 + )] 1456 + #[diagnostic( 1457 + code = "labeler::report::shape_not_400", 1458 + help = "A labeler should return 400 InvalidRequest (not 401 or 500) for a `reasonType` not listed in its published LabelerPolicies.reasonTypes." 1459 + )] 1460 + ShapeNot400 { 1461 + /// Observed status code. 1462 + status: u16, 1463 + /// Error name from the response envelope, if present. 1464 + error_name: Option<String>, 1465 + /// Response body for context. 1466 + #[source_code] 1467 + source_code: NamedSource<Arc<[u8]>>, 1468 + /// Span covering the response body so miette renders `source_code`. 1469 + #[label("rejected with wrong status here")] 1470 + span: SourceSpan, 1471 + }, 1528 1472 1529 - /// Diagnostic for AC4.3: self-mint report rejected by the labeler. 1530 - #[derive(Debug, Error, Diagnostic)] 1531 - #[error("Self-mint report rejected (status {status})")] 1532 - #[diagnostic( 1533 - code = "labeler::report::self_mint_rejected", 1534 - help = "A labeler that advertises reportable shape should accept a well-formed, authenticated createReport. Check the labeler's service-auth validation and its acceptance of the advertised reasonType/subject shape." 1535 - )] 1536 - pub struct SelfMintRejected { 1537 - /// Observed HTTP status code. 1538 - pub status: u16, 1539 - /// Response body for context. 1540 - #[source_code] 1541 - pub source_code: NamedSource<Arc<[u8]>>, 1542 - /// Span covering the response body so miette renders `source_code`. 1543 - #[label("rejected here")] 1544 - pub span: SourceSpan, 1545 - } 1473 + /// Diagnostic for AC4.3: self-mint report rejected by the labeler. 1474 + #[error("Self-mint report rejected (status {status})")] 1475 + #[diagnostic( 1476 + code = "labeler::report::self_mint_rejected", 1477 + help = "A labeler that advertises reportable shape should accept a well-formed, authenticated createReport. Check the labeler's service-auth validation and its acceptance of the advertised reasonType/subject shape." 1478 + )] 1479 + SelfMintRejected { 1480 + /// Observed HTTP status code. 1481 + status: u16, 1482 + /// Response body for context. 1483 + #[source_code] 1484 + source_code: NamedSource<Arc<[u8]>>, 1485 + /// Span covering the response body so miette renders `source_code`. 1486 + #[label("rejected here")] 1487 + span: SourceSpan, 1488 + }, 1546 1489 1547 - /// Diagnostic for AC5.2: labeler rejected PDS-minted service-auth JWT. 1548 - #[derive(Debug, Error, Diagnostic)] 1549 - #[error("Labeler rejected PDS-minted service-auth JWT (status {status})")] 1550 - #[diagnostic( 1551 - code = "labeler::report::pds_service_auth_rejected", 1552 - help = "The PDS issued a service-auth JWT for this user bound to the labeler's DID and the createReport NSID; the labeler should have accepted it." 1553 - )] 1554 - pub struct PdsServiceAuthRejected { 1555 - /// Observed HTTP status code. 1556 - pub status: u16, 1557 - /// Response body for context. 1558 - #[source_code] 1559 - pub source_code: NamedSource<Arc<[u8]>>, 1560 - /// Span covering the response body so miette renders `source_code`. 1561 - #[label("rejected here")] 1562 - pub span: SourceSpan, 1563 - } 1490 + /// Diagnostic for AC5.2: labeler rejected PDS-minted service-auth JWT. 1491 + #[error("Labeler rejected PDS-minted service-auth JWT (status {status})")] 1492 + #[diagnostic( 1493 + code = "labeler::report::pds_service_auth_rejected", 1494 + help = "The PDS issued a service-auth JWT for this user bound to the labeler's DID and the createReport NSID; the labeler should have accepted it." 1495 + )] 1496 + PdsServiceAuthRejected { 1497 + /// Observed HTTP status code. 1498 + status: u16, 1499 + /// Response body for context. 1500 + #[source_code] 1501 + source_code: NamedSource<Arc<[u8]>>, 1502 + /// Span covering the response body so miette renders `source_code`. 1503 + #[label("rejected here")] 1504 + span: SourceSpan, 1505 + }, 1564 1506 1565 - /// Diagnostic for AC6.2: labeler rejected PDS-proxied createReport. 1566 - #[derive(Debug, Error, Diagnostic)] 1567 - #[error("Labeler rejected PDS-proxied createReport (status {status})")] 1568 - #[diagnostic( 1569 - code = "labeler::report::pds_proxied_rejected", 1570 - help = "The PDS forwarded the createReport call on the user's behalf; the downstream labeler reached it but rejected the submission." 1571 - )] 1572 - pub struct PdsProxiedRejected { 1573 - /// Observed HTTP status code. 1574 - pub status: u16, 1575 - /// Response body for context. 1576 - #[source_code] 1577 - pub source_code: NamedSource<Arc<[u8]>>, 1578 - /// Span covering the response body so miette renders `source_code`. 1579 - #[label("rejected here")] 1580 - pub span: SourceSpan, 1507 + /// Diagnostic for AC6.2: labeler rejected PDS-proxied createReport. 1508 + #[error("Labeler rejected PDS-proxied createReport (status {status})")] 1509 + #[diagnostic( 1510 + code = "labeler::report::pds_proxied_rejected", 1511 + help = "The PDS forwarded the createReport call on the user's behalf; the downstream labeler reached it but rejected the submission." 1512 + )] 1513 + PdsProxiedRejected { 1514 + /// Observed HTTP status code. 1515 + status: u16, 1516 + /// Response body for context. 1517 + #[source_code] 1518 + source_code: NamedSource<Arc<[u8]>>, 1519 + /// Span covering the response body so miette renders `source_code`. 1520 + #[label("rejected here")] 1521 + span: SourceSpan, 1522 + }, 1581 1523 } 1582 1524 1583 1525 /// Construct a `NamedSource` and a span covering the whole body.
+43 -3
src/commands/test/labeler/create_report/self_mint.rs
··· 2 2 //! server, and a reference curve. Exposes a single method for signing 3 3 //! atproto service-auth JWTs with that identity. 4 4 5 + use std::net::SocketAddr; 5 6 use std::time::Duration; 6 7 8 + use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode}; 7 9 use serde_json::json; 10 + use url::Url; 8 11 9 12 // Reach `rand_core` through `k256`'s re-export so we pin the same version 10 13 // that `k256::ecdsa::SigningKey::random` expects. 11 14 use k256::elliptic_curve::rand_core; 12 15 13 16 use crate::commands::test::labeler::create_report::did_doc_server::DidDocServer; 14 - use crate::commands::test::labeler::create_report::{self_mint_base_url, self_mint_did_for}; 15 17 use crate::common::identity::{AnySigningKey, Did, encode_multikey}; 16 18 use crate::common::jwt::{self, JwtClaims, JwtHeader}; 17 19 ··· 91 93 let multikey_for_builder = multikey.clone(); 92 94 93 95 let server = DidDocServer::spawn(move |addr| { 94 - let did = self_mint_did_for(addr); 96 + let did = did_for(addr); 95 97 let did_doc = json!({ 96 98 "@context": ["https://www.w3.org/ns/did/v1"], 97 99 "id": did.0, ··· 130 132 131 133 /// URL the labeler will fetch to resolve the DID document. 132 134 pub fn did_doc_url(&self) -> url::Url { 133 - let mut u = self_mint_base_url(self.did_doc_server.local_addr()); 135 + let mut u = base_url(self.did_doc_server.local_addr()); 134 136 u.set_path("/.well-known/did.json"); 135 137 u 136 138 } ··· 166 168 } 167 169 } 168 170 171 + /// Construct `did:web:127.0.0.1%3A{port}` for a self-mint identity bound 172 + /// to the given local `SocketAddr`. The `:` between the IP and the port is 173 + /// percent-encoded per atproto did:web rules. 174 + /// 175 + /// Uses the `SocketAddr` IP literally (typically `127.0.0.1`). IPv6 176 + /// loopback would produce `did:web:::1%3A{port}` which the atproto did 177 + /// syntax regex rejects; for v1 the self-mint server is IPv4-only. 178 + pub(crate) fn did_for(addr: SocketAddr) -> Did { 179 + assert!(addr.is_ipv4(), "self-mint DidDocServer is IPv4-only"); 180 + let host = addr.ip().to_string(); 181 + let port = addr.port(); 182 + // Percent-encode the `:` (and, defensively, any other non-alphanumeric) 183 + // with the standard set. For the `127.0.0.1:{port}` case this yields 184 + // exactly `127.0.0.1%3A{port}`. 185 + let encoded_hostport = format!("{host}{}{port}", utf8_percent_encode(":", NON_ALPHANUMERIC)); 186 + Did(format!("did:web:{encoded_hostport}")) 187 + } 188 + 189 + /// Base URL the labeler uses to fetch the self-mint DID document: 190 + /// `http://127.0.0.1:{port}`. 191 + pub(crate) fn base_url(addr: SocketAddr) -> Url { 192 + Url::parse(&format!("http://{addr}")).expect("SocketAddr Display is always a valid authority") 193 + } 194 + 169 195 #[cfg(test)] 170 196 mod tests { 171 197 use super::*; 172 198 use crate::common::identity::{AnyVerifyingKey, parse_multikey}; 173 199 use crate::common::jwt::verify_compact; 200 + 201 + #[test] 202 + fn self_mint_did_encodes_colon() { 203 + let addr: SocketAddr = "127.0.0.1:5000".parse().unwrap(); 204 + let did = did_for(addr); 205 + assert_eq!(did.0, "did:web:127.0.0.1%3A5000"); 206 + } 207 + 208 + #[test] 209 + fn self_mint_base_url_uses_http() { 210 + let addr: SocketAddr = "127.0.0.1:5000".parse().unwrap(); 211 + let url = base_url(addr); 212 + assert_eq!(url.as_str(), "http://127.0.0.1:5000/"); 213 + } 174 214 175 215 async fn round_trip(curve: SelfMintCurve, expected_alg: &str) { 176 216 let signer = SelfMintSigner::spawn(curve).await.expect("spawn");
+2 -4
tests/common/mod.rs
··· 250 250 match script { 251 251 FakeCreateReportResponse::Transport { message } => { 252 252 Err(CreateReportStageError::Transport { 253 - message, 254 - source: None, 253 + source: Box::new(std::io::Error::other(message)), 255 254 }) 256 255 } 257 256 FakeCreateReportResponse::Response { ··· 354 353 355 354 match script { 356 355 FakePdsXrpcResponse::Transport { message } => Err(CreateReportStageError::Transport { 357 - message, 358 - source: None, 356 + source: Box::new(std::io::Error::other(message)), 359 357 }), 360 358 FakePdsXrpcResponse::Response { status, body } => { 361 359 let raw_body: Arc<[u8]> = Arc::from(body.as_slice());