atproto-client 0.4.1

HTTP client for AT Protocol services with OAuth and identity integration
Documentation
# 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.