atproto-client#
A Rust HTTP client library for AT Protocol services, providing authenticated and unauthenticated HTTP operations with DPoP (Demonstration of Proof-of-Possession) authentication support.
Overview#
atproto-client provides HTTP client functionality specifically designed for interacting with AT Protocol endpoints. This library handles both basic HTTP operations and advanced DPoP-authenticated requests required for secure AT Protocol communication, including full support for repository record operations.
This project was extracted from the open-sourced Smokesignal project and is designed to be a standalone, reusable library for AT Protocol HTTP client operations.
Features#
- HTTP Client Operations: Authenticated and unauthenticated HTTP GET/POST requests with JSON support
- DPoP Authentication: RFC 9449 Demonstration of Proof-of-Possession with automatic retry middleware
- Repository Operations: Complete CRUD operations for AT Protocol repository records
- URL Building: Flexible URL construction with parameter encoding and query string generation
- Error Handling: Structured error types with detailed error codes and messages
- OAuth Integration: Seamless integration with
atproto-oauthfor DPoP authentication - Automatic Retries: Built-in DPoP nonce retry middleware for robust authentication
- Structured Logging: Built-in tracing support for debugging and monitoring
Installation#
Add this to your Cargo.toml:
[dependencies]
atproto-client = "0.5.0"
Usage#
Basic HTTP Operations#
use atproto_client::client;
use reqwest::Client;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let http_client = Client::new();
// Unauthenticated GET request
let response = client::get_json(&http_client, "https://api.example.com/data").await?;
println!("Response: {}", response);
Ok(())
}
DPoP Authentication#
use atproto_client::client::{DPoPAuth, get_dpop_json, post_dpop_json};
use atproto_identity::key::identify_key;
use reqwest::Client;
use serde_json::json;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let http_client = Client::new();
// Set up DPoP authentication
let dpop_auth = DPoPAuth {
dpop_private_key_data: identify_key("did:key:zQ3shNz...")?,
oauth_access_token: "your_access_token".to_string(),
oauth_issuer: "did:plc:issuer123".to_string(),
};
// Authenticated GET request
let response = get_dpop_json(
&http_client,
&dpop_auth,
"https://pds.example.com/xrpc/com.atproto.repo.listRecords?repo=did:plc:user123&collection=app.bsky.feed.post"
).await?;
// Authenticated POST request
let record_data = json!({
"$type": "app.bsky.feed.post",
"text": "Hello AT Protocol!",
"createdAt": "2024-01-01T00:00:00Z"
});
let post_response = post_dpop_json(
&http_client,
&dpop_auth,
"https://pds.example.com/xrpc/com.atproto.repo.createRecord",
record_data
).await?;
println!("Created record: {}", post_response);
Ok(())
}
Repository Operations#
use atproto_client::com::atproto::repo::{
get_record, list_records, create_record, put_record,
CreateRecordRequest, PutRecordRequest, ListRecordsParams
};
use atproto_client::client::DPoPAuth;
use atproto_identity::key::identify_key;
use reqwest::Client;
use serde_json::json;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let http_client = Client::new();
let pds_url = "https://pds.example.com";
let dpop_auth = DPoPAuth {
dpop_private_key_data: identify_key("did:key:zQ3shNz...")?,
oauth_access_token: "your_access_token".to_string(),
oauth_issuer: "did:plc:issuer123".to_string(),
};
// Get a specific record
let record_response = get_record(
&http_client,
&dpop_auth,
pds_url,
"did:plc:user123",
"app.bsky.feed.post",
"3l2uygzaf5c2b",
None // Optional CID for specific version
).await?;
// List records in a collection with parameters
let list_response = list_records::<serde_json::Value>(
&http_client,
&dpop_auth,
pds_url,
"did:plc:user123".to_string(),
"app.bsky.feed.post".to_string(),
ListRecordsParams::new()
.limit(50)
.reverse(false)
).await?;
// Create a new record
let create_request = CreateRecordRequest {
repo: "did:plc:user123".to_string(),
collection: "app.bsky.feed.post".to_string(),
record_key: None, // Let server generate key
validate: true,
record: json!({
"$type": "app.bsky.feed.post",
"text": "Hello from atproto-client!",
"createdAt": "2024-01-01T00:00:00Z"
}),
swap_commit: None,
};
let create_response = create_record(
&http_client,
&dpop_auth,
pds_url,
create_request
).await?;
// Update a record with specific key
let put_request = PutRecordRequest {
repo: "did:plc:user123".to_string(),
collection: "app.bsky.feed.post".to_string(),
record_key: "3l2uygzaf5c2b".to_string(),
validate: true,
record: json!({
"$type": "app.bsky.feed.post",
"text": "Updated post content",
"createdAt": "2024-01-01T00:00:00Z"
}),
swap_commit: None,
swap_record: None,
};
let put_response = put_record(
&http_client,
&dpop_auth,
pds_url,
put_request
).await?;
Ok(())
}
URL Building#
use atproto_client::url::{URLBuilder, build_url};
fn main() {
// Using URLBuilder for complex URLs
let mut builder = URLBuilder::new("pds.example.com");
builder.path("/xrpc/com.atproto.repo.listRecords");
builder.param("repo", "did:plc:user123");
builder.param("collection", "app.bsky.feed.post");
builder.param("limit", "50");
let url = builder.build();
// Result: "https://pds.example.com/xrpc/com.atproto.repo.listRecords?repo=did%3Aplc%3Auser123&collection=app.bsky.feed.post&limit=50"
// Using convenience function for simple URLs
let simple_url = build_url(
"pds.example.com",
"/xrpc/com.atproto.repo.getRecord",
vec![
Some(("repo", "did:plc:user123")),
Some(("collection", "app.bsky.feed.post")),
Some(("rkey", "3l2uygzaf5c2b")),
None, // Optional parameters can be None
]
);
println!("Built URL: {}", simple_url);
}
AT Protocol Repository Operations#
The com::atproto::repo module provides client functions for the core AT Protocol repository XRPC methods:
Supported Operations#
get_record(): Retrieve a specific record by repository, collection, and record keylist_records(): List records in a collection with pagination and filtering supportcreate_record(): Create a new record in a repository with optional record keyput_record(): Update or create a record with a specific record key
Request/Response Types#
ListRecordsParams: Builder-style parameters for listing records with paginationCreateRecordRequest<T>: Strongly-typed request for creating new recordsPutRecordRequest<T>: Strongly-typed request for updating recordsGetRecordResponse: Response containing record data, URI, and CIDListRecordsResponse<T>: Paginated response with cursor supportCreateRecordResponse: Response with created record URI and CIDPutRecordResponse: Response with updated record URI and CID
All operations support:
- Generic record types with serde serialization/deserialization
- Validation options for record schema compliance
- Atomic commit operations with swap parameters
- Comprehensive error handling with structured error types
Modules#
- [
client] - Core HTTP client operations with DPoP authentication support - [
com::atproto::repo] - AT Protocol repository operations for record management - [
url] - URL construction utilities with parameter encoding - [
errors] - Structured error types for client operations
Error Handling#
The crate uses structured error types with detailed error codes:
use atproto_client::errors::{ClientError, DPoPError};
// Example error handling
match result {
Err(ClientError::HttpRequestFailed { url, error }) => {
println!("HTTP request to {} failed: {}", url, error);
},
Err(ClientError::JsonParseFailed { url, error }) => {
println!("JSON parsing failed for {}: {}", url, error);
},
Ok(response) => println!("Success: {:?}", response),
}
// DPoP authentication errors
match dpop_result {
Err(DPoPError::ProofGenerationFailed { error }) => {
println!("DPoP proof generation failed: {}", error);
},
Err(DPoPError::HttpRequestFailed { url, error }) => {
println!("DPoP authenticated request to {} failed: {}", url, error);
},
Ok(response) => println!("Authenticated request successful: {:?}", response),
}
Error Format#
All errors follow the standardized format:
error-atproto-client-<domain>-<number> <message>: <details>
Example error codes:
error-atproto-client-http-1- HTTP request failureserror-atproto-client-http-2- JSON parsing failureserror-atproto-client-dpop-1- DPoP proof generation failureserror-atproto-client-dpop-2- DPoP authenticated request failureserror-atproto-client-dpop-3- DPoP response parsing failures
Authentication#
DPoP Authentication#
The library supports DPoP (Demonstration of Proof-of-Possession) authentication as specified in RFC 9449:
- Automatic DPoP proof generation for each request
- Built-in retry middleware for nonce-based challenges
- Integration with OAuth access tokens
- Support for both authorization and resource requests
Key Requirements#
- Private key for DPoP proof signing (P-256 or K-256)
- OAuth access token from authorization server
- Issuer identifier for proof validation
Dependencies#
This crate builds on:
atproto-identity- Cryptographic key operations and DID resolutionatproto-record- AT Protocol record operationsatproto-oauth- OAuth 2.0 and DPoP implementationreqwest- HTTP client for network operationsreqwest-middleware- HTTP middleware support for DPoP retry logicreqwest-chain- Middleware chaining for authentication flowsserde_json- JSON serialization for AT Protocol data structurestokio- Async runtime for HTTP operationstracing- Structured logging for debugging and monitoringthiserror- Structured error type derivation
Library Only#
This crate is designed as a library and does not provide command line tools. All functionality is accessed programmatically through the Rust API. For command line operations, see the atproto-identity and atproto-record crates which include CLI tools for identity resolution and record signing operations.
Contributing#
Contributions are welcome! Please ensure that:
- All tests pass:
cargo test - Code is properly formatted:
cargo fmt - No linting issues:
cargo clippy - New functionality includes appropriate tests and documentation
- Error handling follows the project's structured error format
License#
This project is licensed under the MIT License. See the LICENSE file for details.
Acknowledgments#
This library was extracted from the Smokesignal project, an open-source event and RSVP management and discovery application.