# 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](https://tangled.sh/@smokesignal.events/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-oauth` for 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`:
```toml
[dependencies]
atproto-client = "0.4.1"
```
## Usage
### Basic HTTP Operations
```rust
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
```rust
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
```rust
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
```rust
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 key
- **`list_records()`**: List records in a collection with pagination and filtering support
- **`create_record()`**: Create a new record in a repository with optional record key
- **`put_record()`**: Update or create a record with a specific record key
### Request/Response Types
- **`ListRecordsParams`**: Builder-style parameters for listing records with pagination
- **`CreateRecordRequest<T>`**: Strongly-typed request for creating new records
- **`PutRecordRequest<T>`**: Strongly-typed request for updating records
- **`GetRecordResponse`**: Response containing record data, URI, and CID
- **`ListRecordsResponse<T>`**: Paginated response with cursor support
- **`CreateRecordResponse`**: Response with created record URI and CID
- **`PutRecordResponse`**: 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:
```rust
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 failures
- `error-atproto-client-http-2` - JSON parsing failures
- `error-atproto-client-dpop-1` - DPoP proof generation failures
- `error-atproto-client-dpop-2` - DPoP authenticated request failures
- `error-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`](../atproto-identity) - Cryptographic key operations and DID resolution
- [`atproto-record`](../atproto-record) - AT Protocol record operations
- [`atproto-oauth`](../atproto-oauth) - OAuth 2.0 and DPoP implementation
- `reqwest` - HTTP client for network operations
- `reqwest-middleware` - HTTP middleware support for DPoP retry logic
- `reqwest-chain` - Middleware chaining for authentication flows
- `serde_json` - JSON serialization for AT Protocol data structures
- `tokio` - Async runtime for HTTP operations
- `tracing` - Structured logging for debugging and monitoring
- `thiserror` - 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`](../atproto-identity) and [`atproto-record`](../atproto-record) crates which include CLI tools for identity resolution and record signing operations.
## Contributing
Contributions are welcome! Please ensure that:
1. All tests pass: `cargo test`
2. Code is properly formatted: `cargo fmt`
3. No linting issues: `cargo clippy`
4. New functionality includes appropriate tests and documentation
5. 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](https://tangled.sh/@smokesignal.events/smokesignal) project, an open-source event and RSVP management and discovery application.