A library for ATProtocol identities.
README.md

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

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:

  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.