atproto-client 0.6.0

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 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:

[dependencies]
atproto-client = "0.6.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 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:

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 - Cryptographic key operations and DID resolution
  • atproto-record - AT Protocol record operations
  • 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

Command Line Tools

The crate includes one command-line tool for DPoP authentication testing:

atproto-client-dpop

A command-line tool for testing DPoP authentication flows with AT Protocol services. This tool demonstrates the complete DPoP authentication process including proof generation, HTTP request signing, and token usage.

Features:

  • DPoP Proof Generation: Creates DPoP proofs for HTTP requests using private keys
  • OAuth Integration: Supports OAuth access tokens with DPoP binding
  • HTTP Client Testing: Tests DPoP authentication against real AT Protocol endpoints
  • Request Signing: Demonstrates proper DPoP header generation and validation
  • Token Management: Shows how to use DPoP-bound access tokens for API requests
# Test DPoP authentication with an AT Protocol endpoint
cargo run --bin atproto-client-dpop \
  --private-key did:key:zQ3shNzMp4oaaQ1gQRzCxMGXFrSW3NEM1M9T6KCY9eA7HhyEA \
  --access-token your_access_token \
  --issuer did:plc:issuer123 \
  --url https://pds.example.com/xrpc/com.atproto.repo.listRecords \
  --method GET

# Example POST request with DPoP authentication
cargo run --bin atproto-client-dpop \
  --private-key did:key:zQ3shNzMp4oaaQ1gQRzCxMGXFrSW3NEM1M9T6KCY9eA7HhyEA \
  --access-token your_access_token \
  --issuer did:plc:issuer123 \
  --url https://pds.example.com/xrpc/com.atproto.repo.createRecord \
  --method POST \
  --data '{"repo":"did:plc:user123","collection":"app.bsky.feed.post","record":{"$type":"app.bsky.feed.post","text":"Hello AT Protocol!"}}'

Arguments:

  • --private-key - DID key string for DPoP proof signing
  • --access-token - OAuth access token for authentication
  • --issuer - Issuer DID for proof validation
  • --url - Target URL for the authenticated request
  • --method - HTTP method (GET, POST, PUT, DELETE)
  • --data - Optional JSON data for POST/PUT requests

This tool is useful for:

  • Testing DPoP implementation against AT Protocol services
  • Validating authentication flows during development
  • Debugging DPoP proof generation and validation
  • Learning how DPoP authentication works in practice

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 project, an open-source event and RSVP management and discovery application.