A library for ATProtocol identities.

refactor: linting and formatting

Changed files
+194 -85
crates
atproto-client
atproto-jetstream
atproto-oauth
atproto-oauth-aip
+1 -1
crates/atproto-client/src/client.rs
··· 42 42 /// DPoP authentication with proof-of-possession tokens and OAuth access token 43 43 DPoP(DPoPAuth), 44 44 /// App password authentication using JWT bearer tokens 45 - AppPassword(AppPasswordAuth) 45 + AppPassword(AppPasswordAuth), 46 46 } 47 47 48 48 /// Performs an unauthenticated HTTP GET request and parses the response as JSON.
+2 -2
crates/atproto-client/src/com_atproto_identity.rs
··· 9 9 use serde::{Deserialize, de::DeserializeOwned}; 10 10 11 11 use crate::{ 12 - client::{get_apppassword_json, get_dpop_json, get_json, Auth}, 12 + client::{Auth, get_apppassword_json, get_dpop_json, get_json}, 13 13 errors::SimpleError, 14 - url::URLBuilder 14 + url::URLBuilder, 15 15 }; 16 16 17 17 /// Response from the com.atproto.identity.resolveHandle XRPC method.
+4 -1
crates/atproto-client/src/com_atproto_repo.rs
··· 29 29 use serde::{Deserialize, Serialize, de::DeserializeOwned}; 30 30 31 31 use crate::{ 32 - client::{Auth, get_apppassword_json, get_bytes, get_dpop_json, get_json, post_apppassword_json, post_dpop_json, post_json}, 32 + client::{ 33 + Auth, get_apppassword_json, get_bytes, get_dpop_json, get_json, post_apppassword_json, 34 + post_dpop_json, post_json, 35 + }, 33 36 errors::SimpleError, 34 37 url::URLBuilder, 35 38 };
+1 -1
crates/atproto-client/src/lib.rs
··· 22 22 pub mod errors; 23 23 pub mod url; 24 24 25 + mod com_atproto_identity; 25 26 mod com_atproto_repo; 26 27 mod com_atproto_server; 27 - mod com_atproto_identity; 28 28 29 29 /// AT Protocol namespace modules. 30 30 pub mod com {
+149 -48
crates/atproto-jetstream/src/consumer.rs
··· 456 456 #[test] 457 457 fn test_parse_account_event() { 458 458 let json_str = r#"{"did":"did:plc:yn72uqr4ihkjfbz7us7buqsq","time_us":1757517640675638,"kind":"account","account":{"active":false,"did":"did:plc:yn72uqr4ihkjfbz7us7buqsq","seq":13206502767,"status":"takendown","time":"2025-09-10T15:20:40.439Z"}}"#; 459 - 459 + 460 460 let event = serde_json::from_str::<JetstreamEvent>(json_str) 461 461 .expect("Failed to parse account event JSON"); 462 - 462 + 463 463 match event { 464 - JetstreamEvent::Account { did, time_us, kind, account: identity } => { 464 + JetstreamEvent::Account { 465 + did, 466 + time_us, 467 + kind, 468 + account: identity, 469 + } => { 465 470 assert_eq!(did, "did:plc:yn72uqr4ihkjfbz7us7buqsq"); 466 471 assert_eq!(time_us, 1757517640675638); 467 472 assert_eq!(kind, "account"); 468 - 473 + 469 474 // Verify the account data structure 470 475 assert!(identity.is_object()); 471 476 let account_obj = identity.as_object().unwrap(); 472 - assert_eq!(account_obj.get("active").unwrap(), &serde_json::json!(false)); 473 - assert_eq!(account_obj.get("did").unwrap(), &serde_json::json!("did:plc:yn72uqr4ihkjfbz7us7buqsq")); 474 - assert_eq!(account_obj.get("seq").unwrap(), &serde_json::json!(13206502767i64)); 475 - assert_eq!(account_obj.get("status").unwrap(), &serde_json::json!("takendown")); 476 - assert_eq!(account_obj.get("time").unwrap(), &serde_json::json!("2025-09-10T15:20:40.439Z")); 477 + assert_eq!( 478 + account_obj.get("active").unwrap(), 479 + &serde_json::json!(false) 480 + ); 481 + assert_eq!( 482 + account_obj.get("did").unwrap(), 483 + &serde_json::json!("did:plc:yn72uqr4ihkjfbz7us7buqsq") 484 + ); 485 + assert_eq!( 486 + account_obj.get("seq").unwrap(), 487 + &serde_json::json!(13206502767i64) 488 + ); 489 + assert_eq!( 490 + account_obj.get("status").unwrap(), 491 + &serde_json::json!("takendown") 492 + ); 493 + assert_eq!( 494 + account_obj.get("time").unwrap(), 495 + &serde_json::json!("2025-09-10T15:20:40.439Z") 496 + ); 477 497 } 478 498 _ => panic!("Expected JetstreamEvent::Account variant, got {:?}", event), 479 499 } ··· 482 502 #[test] 483 503 fn test_parse_identity_event() { 484 504 let json_str = r#"{"did":"did:plc:mbuadp4xzlbmc2ncqp3pmtox","time_us":1757517628039893,"kind":"identity","identity":{"did":"did:plc:mbuadp4xzlbmc2ncqp3pmtox","handle":"nhieothv.bsky.social","seq":13206497272,"time":"2025-09-10T15:20:27.610Z"}}"#; 485 - 505 + 486 506 let event = serde_json::from_str::<JetstreamEvent>(json_str) 487 507 .expect("Failed to parse identity event JSON"); 488 - 508 + 489 509 match event { 490 - JetstreamEvent::Identity { did, time_us, kind, identity } => { 510 + JetstreamEvent::Identity { 511 + did, 512 + time_us, 513 + kind, 514 + identity, 515 + } => { 491 516 assert_eq!(did, "did:plc:mbuadp4xzlbmc2ncqp3pmtox"); 492 517 assert_eq!(time_us, 1757517628039893); 493 518 assert_eq!(kind, "identity"); 494 - 519 + 495 520 // Verify the identity data structure 496 521 assert!(identity.is_object()); 497 522 let identity_obj = identity.as_object().unwrap(); 498 - assert_eq!(identity_obj.get("did").unwrap(), &serde_json::json!("did:plc:mbuadp4xzlbmc2ncqp3pmtox")); 499 - assert_eq!(identity_obj.get("handle").unwrap(), &serde_json::json!("nhieothv.bsky.social")); 500 - assert_eq!(identity_obj.get("seq").unwrap(), &serde_json::json!(13206497272i64)); 501 - assert_eq!(identity_obj.get("time").unwrap(), &serde_json::json!("2025-09-10T15:20:27.610Z")); 523 + assert_eq!( 524 + identity_obj.get("did").unwrap(), 525 + &serde_json::json!("did:plc:mbuadp4xzlbmc2ncqp3pmtox") 526 + ); 527 + assert_eq!( 528 + identity_obj.get("handle").unwrap(), 529 + &serde_json::json!("nhieothv.bsky.social") 530 + ); 531 + assert_eq!( 532 + identity_obj.get("seq").unwrap(), 533 + &serde_json::json!(13206497272i64) 534 + ); 535 + assert_eq!( 536 + identity_obj.get("time").unwrap(), 537 + &serde_json::json!("2025-09-10T15:20:27.610Z") 538 + ); 502 539 } 503 540 _ => panic!("Expected JetstreamEvent::Identity variant, got {:?}", event), 504 541 } ··· 507 544 #[test] 508 545 fn test_parse_delete_event() { 509 546 let json_str = r#"{"did":"did:plc:5ozthefrqdo5kqnxzfgthhpp","time_us":1757519323847323,"kind":"commit","commit":{"rev":"3lyileto4q52k","operation":"delete","collection":"app.bsky.graph.follow","rkey":"3lxqxntaew32z"}}"#; 510 - 547 + 511 548 let event = serde_json::from_str::<JetstreamEvent>(json_str) 512 549 .expect("Failed to parse delete event JSON"); 513 - 550 + 514 551 match event { 515 - JetstreamEvent::Delete { did, time_us, kind, commit } => { 552 + JetstreamEvent::Delete { 553 + did, 554 + time_us, 555 + kind, 556 + commit, 557 + } => { 516 558 assert_eq!(did, "did:plc:5ozthefrqdo5kqnxzfgthhpp"); 517 559 assert_eq!(time_us, 1757519323847323); 518 560 assert_eq!(kind, "commit"); 519 - 561 + 520 562 // Verify the delete operation details 521 563 assert_eq!(commit.rev, "3lyileto4q52k"); 522 564 assert_eq!(commit.operation, "delete"); ··· 530 572 #[test] 531 573 fn test_parse_commit_event() { 532 574 let json_str = r#"{"did":"did:plc:suq5ijgyqmsawwf5tskf654x","time_us":1757519323848962,"kind":"commit","commit":{"rev":"3lyiletdopl2c","operation":"create","collection":"app.bsky.feed.like","rkey":"3lyiletddxt2c","record":{"$type":"app.bsky.feed.like","createdAt":"2025-09-10T15:47:13.086Z","subject":{"cid":"bafyreib2pygab7z5l7nkqf6bchcvgt4jwsqiaenpf3sr65lugum2uvzzf4","uri":"at://did:plc:yw65rktdby2chplqdytqzcao/app.bsky.feed.post/3lyildyjxgs2o"}},"cid":"bafyreigroo6vhxt62ufcndhaxzas6btq4jmniuz4egszbwuqgiyisqwqoy"}}"#; 533 - 575 + 534 576 let event = serde_json::from_str::<JetstreamEvent>(json_str) 535 577 .expect("Failed to parse commit event JSON"); 536 - 578 + 537 579 match event { 538 - JetstreamEvent::Commit { did, time_us, kind, commit } => { 580 + JetstreamEvent::Commit { 581 + did, 582 + time_us, 583 + kind, 584 + commit, 585 + } => { 539 586 assert_eq!(did, "did:plc:suq5ijgyqmsawwf5tskf654x"); 540 587 assert_eq!(time_us, 1757519323848962); 541 588 assert_eq!(kind, "commit"); 542 - 589 + 543 590 // Verify the commit operation details 544 591 assert_eq!(commit.rev, "3lyiletdopl2c"); 545 592 assert_eq!(commit.operation, "create"); 546 593 assert_eq!(commit.collection, "app.bsky.feed.like"); 547 594 assert_eq!(commit.rkey, "3lyiletddxt2c"); 548 - assert_eq!(commit.cid, "bafyreigroo6vhxt62ufcndhaxzas6btq4jmniuz4egszbwuqgiyisqwqoy"); 549 - 595 + assert_eq!( 596 + commit.cid, 597 + "bafyreigroo6vhxt62ufcndhaxzas6btq4jmniuz4egszbwuqgiyisqwqoy" 598 + ); 599 + 550 600 // Verify the record data structure 551 601 assert!(commit.record.is_object()); 552 602 let record_obj = commit.record.as_object().unwrap(); 553 - assert_eq!(record_obj.get("$type").unwrap(), &serde_json::json!("app.bsky.feed.like")); 554 - assert_eq!(record_obj.get("createdAt").unwrap(), &serde_json::json!("2025-09-10T15:47:13.086Z")); 555 - 603 + assert_eq!( 604 + record_obj.get("$type").unwrap(), 605 + &serde_json::json!("app.bsky.feed.like") 606 + ); 607 + assert_eq!( 608 + record_obj.get("createdAt").unwrap(), 609 + &serde_json::json!("2025-09-10T15:47:13.086Z") 610 + ); 611 + 556 612 // Verify the subject within the record 557 613 let subject = record_obj.get("subject").unwrap().as_object().unwrap(); 558 - assert_eq!(subject.get("cid").unwrap(), &serde_json::json!("bafyreib2pygab7z5l7nkqf6bchcvgt4jwsqiaenpf3sr65lugum2uvzzf4")); 559 - assert_eq!(subject.get("uri").unwrap(), &serde_json::json!("at://did:plc:yw65rktdby2chplqdytqzcao/app.bsky.feed.post/3lyildyjxgs2o")); 614 + assert_eq!( 615 + subject.get("cid").unwrap(), 616 + &serde_json::json!( 617 + "bafyreib2pygab7z5l7nkqf6bchcvgt4jwsqiaenpf3sr65lugum2uvzzf4" 618 + ) 619 + ); 620 + assert_eq!( 621 + subject.get("uri").unwrap(), 622 + &serde_json::json!( 623 + "at://did:plc:yw65rktdby2chplqdytqzcao/app.bsky.feed.post/3lyildyjxgs2o" 624 + ) 625 + ); 560 626 } 561 627 _ => panic!("Expected JetstreamEvent::Commit variant, got {:?}", event), 562 628 } ··· 565 631 #[test] 566 632 fn test_parse_commit_update_event() { 567 633 let json_str = r#"{"did":"did:plc:mek6cpladv2xrlu2zdykoxgz","time_us":1757519523286358,"kind":"commit","commit":{"rev":"3lyilmalk762z","operation":"update","collection":"app.bsky.actor.profile","rkey":"self","record":{"$type":"app.bsky.actor.profile","avatar":{"$type":"blob","ref":{"$link":"bafkreibmn7xi5iwugioov463wux62dg4m4w6qqrbsnileaobrzgxdwbsqy"},"mimeType":"image/jpeg","size":289838},"banner":{"$type":"blob","ref":{"$link":"bafkreicjgdlfs6fyyddjklfzrf6w2boychodkdebtjaiwavhcffjtuavsi"},"mimeType":"image/jpeg","size":676693},"description":"ela/dela | parte da fauna fantástica do céu azul | praticamente inofensiva","displayName":"la mucura mística","pinnedPost":{"cid":"bafyreihn2t4efvipbcignd6rlybmoecb7hx4jgntsojhpibjzxno3zhbuq","uri":"at://did:plc:mek6cpladv2xrlu2zdykoxgz/app.bsky.feed.post/3lxarfbd4ts2j"}},"cid":"bafyreifpmgw3podvvm4raq6zewn6jhoa73t7mlgf3f7hty2adb6f2ga7j4"}}"#; 568 - 634 + 569 635 let event = serde_json::from_str::<JetstreamEvent>(json_str) 570 636 .expect("Failed to parse commit update event JSON"); 571 - 637 + 572 638 match event { 573 - JetstreamEvent::Commit { did, time_us, kind, commit } => { 639 + JetstreamEvent::Commit { 640 + did, 641 + time_us, 642 + kind, 643 + commit, 644 + } => { 574 645 assert_eq!(did, "did:plc:mek6cpladv2xrlu2zdykoxgz"); 575 646 assert_eq!(time_us, 1757519523286358); 576 647 assert_eq!(kind, "commit"); 577 - 648 + 578 649 // Verify the commit operation details 579 650 assert_eq!(commit.rev, "3lyilmalk762z"); 580 651 assert_eq!(commit.operation, "update"); 581 652 assert_eq!(commit.collection, "app.bsky.actor.profile"); 582 653 assert_eq!(commit.rkey, "self"); 583 - assert_eq!(commit.cid, "bafyreifpmgw3podvvm4raq6zewn6jhoa73t7mlgf3f7hty2adb6f2ga7j4"); 584 - 654 + assert_eq!( 655 + commit.cid, 656 + "bafyreifpmgw3podvvm4raq6zewn6jhoa73t7mlgf3f7hty2adb6f2ga7j4" 657 + ); 658 + 585 659 // Verify the record data structure 586 660 assert!(commit.record.is_object()); 587 661 let record_obj = commit.record.as_object().unwrap(); 588 - assert_eq!(record_obj.get("$type").unwrap(), &serde_json::json!("app.bsky.actor.profile")); 589 - assert_eq!(record_obj.get("description").unwrap(), &serde_json::json!("ela/dela | parte da fauna fantástica do céu azul | praticamente inofensiva")); 590 - assert_eq!(record_obj.get("displayName").unwrap(), &serde_json::json!("la mucura mística")); 591 - 662 + assert_eq!( 663 + record_obj.get("$type").unwrap(), 664 + &serde_json::json!("app.bsky.actor.profile") 665 + ); 666 + assert_eq!( 667 + record_obj.get("description").unwrap(), 668 + &serde_json::json!( 669 + "ela/dela | parte da fauna fantástica do céu azul | praticamente inofensiva" 670 + ) 671 + ); 672 + assert_eq!( 673 + record_obj.get("displayName").unwrap(), 674 + &serde_json::json!("la mucura mística") 675 + ); 676 + 592 677 // Verify avatar blob 593 678 let avatar = record_obj.get("avatar").unwrap().as_object().unwrap(); 594 679 assert_eq!(avatar.get("$type").unwrap(), &serde_json::json!("blob")); 595 - assert_eq!(avatar.get("mimeType").unwrap(), &serde_json::json!("image/jpeg")); 680 + assert_eq!( 681 + avatar.get("mimeType").unwrap(), 682 + &serde_json::json!("image/jpeg") 683 + ); 596 684 assert_eq!(avatar.get("size").unwrap(), &serde_json::json!(289838)); 597 - 685 + 598 686 // Verify banner blob 599 687 let banner = record_obj.get("banner").unwrap().as_object().unwrap(); 600 688 assert_eq!(banner.get("$type").unwrap(), &serde_json::json!("blob")); 601 - assert_eq!(banner.get("mimeType").unwrap(), &serde_json::json!("image/jpeg")); 689 + assert_eq!( 690 + banner.get("mimeType").unwrap(), 691 + &serde_json::json!("image/jpeg") 692 + ); 602 693 assert_eq!(banner.get("size").unwrap(), &serde_json::json!(676693)); 603 - 694 + 604 695 // Verify pinned post 605 696 let pinned_post = record_obj.get("pinnedPost").unwrap().as_object().unwrap(); 606 - assert_eq!(pinned_post.get("cid").unwrap(), &serde_json::json!("bafyreihn2t4efvipbcignd6rlybmoecb7hx4jgntsojhpibjzxno3zhbuq")); 607 - assert_eq!(pinned_post.get("uri").unwrap(), &serde_json::json!("at://did:plc:mek6cpladv2xrlu2zdykoxgz/app.bsky.feed.post/3lxarfbd4ts2j")); 697 + assert_eq!( 698 + pinned_post.get("cid").unwrap(), 699 + &serde_json::json!( 700 + "bafyreihn2t4efvipbcignd6rlybmoecb7hx4jgntsojhpibjzxno3zhbuq" 701 + ) 702 + ); 703 + assert_eq!( 704 + pinned_post.get("uri").unwrap(), 705 + &serde_json::json!( 706 + "at://did:plc:mek6cpladv2xrlu2zdykoxgz/app.bsky.feed.post/3lxarfbd4ts2j" 707 + ) 708 + ); 608 709 } 609 710 _ => panic!("Expected JetstreamEvent::Commit variant, got {:?}", event), 610 711 }
+11 -4
crates/atproto-oauth-aip/src/workflow.rs
··· 112 112 //! and protocol violations. 113 113 114 114 use anyhow::Result; 115 - use atproto_oauth::workflow::{OAuthRequest, OAuthRequestState, ParResponse, TokenResponse}; 115 + use atproto_oauth::{ 116 + jwk::WrappedJsonWebKey, 117 + workflow::{OAuthRequest, OAuthRequestState, ParResponse, TokenResponse}, 118 + }; 116 119 use serde::Deserialize; 117 120 118 121 use crate::errors::OAuthWorkflowError; ··· 176 179 #[cfg_attr(feature = "zeroize", zeroize(skip))] 177 180 pub pds_endpoint: String, 178 181 182 + /// The DPoP (Demonstration of Proof-of-Possession) key in string serialized format. 183 + #[cfg_attr(feature = "zeroize", zeroize(skip))] 184 + pub dpop_key: Option<String>, 185 + 179 186 /// The DPoP (Demonstration of Proof-of-Possession) key in JWK format. 180 187 #[cfg_attr(feature = "zeroize", zeroize(skip))] 181 - pub dpop_key: String, 188 + pub dpop_jwk: Option<WrappedJsonWebKey>, 182 189 183 190 /// Unix timestamp indicating when this session expires. 184 191 #[cfg_attr(feature = "zeroize", zeroize(skip))] ··· 189 196 #[cfg_attr(feature = "zeroize", derive(Zeroize, ZeroizeOnDrop))] 190 197 #[serde(untagged)] 191 198 enum WrappedATProtocolSession { 192 - ATProtocolSession(ATProtocolSession), 199 + ATProtocolSession(Box<ATProtocolSession>), 193 200 194 201 #[cfg_attr(feature = "zeroize", zeroize(skip))] 195 202 Error { ··· 428 435 .map_err(OAuthWorkflowError::SessionResponseParseFailed)?; 429 436 430 437 match response { 431 - WrappedATProtocolSession::ATProtocolSession(ref value) => Ok(value.clone()), 438 + WrappedATProtocolSession::ATProtocolSession(ref value) => Ok(*value.clone()), 432 439 WrappedATProtocolSession::Error { 433 440 ref error, 434 441 ref error_description,
+2 -2
crates/atproto-oauth/src/lib.rs
··· 20 20 pub mod pkce; 21 21 /// OAuth resource and authorization server management. 22 22 pub mod resources; 23 + /// OAuth 2.0 scope definitions and parsing for AT Protocol. 24 + pub mod scopes; 23 25 /// OAuth request storage abstraction for CRUD operations. 24 26 pub mod storage; 25 27 /// LRU-based implementation of OAuth request storage (requires `lru` feature). ··· 27 29 pub mod storage_lru; 28 30 /// OAuth workflow implementation for AT Protocol authorization flows. 29 31 pub mod workflow; 30 - /// OAuth 2.0 scope definitions and parsing for AT Protocol. 31 - pub mod scopes;
+24 -26
crates/atproto-oauth/src/scopes.rs
··· 182 182 } 183 183 184 184 let mut scopes = Vec::new(); 185 - for scope_str in s.trim().split_whitespace() { 185 + for scope_str in s.split_whitespace() { 186 186 scopes.push(Self::parse(scope_str)?); 187 187 } 188 188 ··· 318 318 let mut suffix = None; 319 319 320 320 for prefix in &prefixes { 321 - if s.starts_with(prefix) { 322 - let remainder = &s[prefix.len()..]; 323 - if remainder.is_empty() || remainder.starts_with(':') || remainder.starts_with('?') 324 - { 325 - found_prefix = Some(*prefix); 326 - if remainder.starts_with(':') { 327 - suffix = Some(&remainder[1..]); 328 - } else if remainder.starts_with('?') { 329 - suffix = Some(remainder); 330 - } else { 331 - suffix = None; 332 - } 333 - break; 321 + if let Some(remainder) = s.strip_prefix(prefix) 322 + && (remainder.is_empty() 323 + || remainder.starts_with(':') 324 + || remainder.starts_with('?')) 325 + { 326 + found_prefix = Some(*prefix); 327 + if let Some(stripped) = remainder.strip_prefix(':') { 328 + suffix = Some(stripped); 329 + } else if remainder.starts_with('?') { 330 + suffix = Some(remainder); 331 + } else { 332 + suffix = None; 334 333 } 334 + break; 335 335 } 336 336 } 337 337 ··· 744 744 (_, Scope::Email) => false, 745 745 (Scope::Account(a), Scope::Account(b)) => { 746 746 a.resource == b.resource 747 - && match (a.action, b.action) { 748 - (AccountAction::Manage, _) => true, 749 - (AccountAction::Read, AccountAction::Read) => true, 750 - _ => false, 751 - } 747 + && matches!( 748 + (a.action, b.action), 749 + (AccountAction::Manage, _) | (AccountAction::Read, AccountAction::Read) 750 + ) 752 751 } 753 - (Scope::Identity(a), Scope::Identity(b)) => match (a, b) { 754 - (IdentityScope::All, _) => true, 755 - (IdentityScope::Handle, IdentityScope::Handle) => true, 756 - _ => false, 757 - }, 752 + (Scope::Identity(a), Scope::Identity(b)) => matches!( 753 + (a, b), 754 + (IdentityScope::All, _) | (IdentityScope::Handle, IdentityScope::Handle) 755 + ), 758 756 (Scope::Blob(a), Scope::Blob(b)) => { 759 757 for b_pattern in &b.accept { 760 758 let mut granted = false; ··· 833 831 fn from_str(s: &str) -> Result<Self, Self::Err> { 834 832 if s == "*/*" { 835 833 Ok(MimePattern::All) 836 - } else if s.ends_with("/*") { 837 - Ok(MimePattern::TypeWildcard(s[..s.len() - 2].to_string())) 834 + } else if let Some(stripped) = s.strip_suffix("/*") { 835 + Ok(MimePattern::TypeWildcard(stripped.to_string())) 838 836 } else if s.contains('/') { 839 837 Ok(MimePattern::Exact(s.to_string())) 840 838 } else {