A library for ATProtocol identities.

feature: atproto-record-cid

Changed files
+234 -3
crates
atproto-client
src
atproto-record
+3 -1
CLAUDE.md
··· 30 30 #### Record Operations 31 31 - **Sign records**: `cargo run --features clap --bin atproto-record-sign -- <issuer_did> <signing_key> <record_input> repository=<repo> collection=<collection>` 32 32 - **Verify records**: `cargo run --features clap --bin atproto-record-verify -- <issuer_did> <key> <record_input> repository=<repo> collection=<collection>` 33 + - **Generate CID**: `cat record.json | cargo run --features clap --bin atproto-record-cid` (reads JSON from stdin, outputs CID) 33 34 34 35 #### Client Tools 35 36 - **App password auth**: `cargo run --features clap --bin atproto-client-app-password -- <subject> <access_token> <xrpc_path>` ··· 55 56 - **atproto-xrpcs-helloworld**: Complete example XRPC service 56 57 57 58 Features: 58 - - **12 CLI tools** with consistent clap-based command-line interfaces (optional via `clap` feature) 59 + - **13 CLI tools** with consistent clap-based command-line interfaces (optional via `clap` feature) 59 60 - **Rust edition 2024** with modern async/await patterns 60 61 - **Comprehensive error handling** with structured error types 61 62 - **Full test coverage** with unit tests across all modules ··· 157 158 #### Record Operations (atproto-record) 158 159 - **`src/bin/atproto-record-sign.rs`**: Sign AT Protocol records with cryptographic signatures 159 160 - **`src/bin/atproto-record-verify.rs`**: Verify AT Protocol record signatures 161 + - **`src/bin/atproto-record-cid.rs`**: Generate CID (Content Identifier) for AT Protocol records using DAG-CBOR serialization 160 162 161 163 #### Client Tools (atproto-client) 162 164 - **`src/bin/atproto-client-app-password.rs`**: Make XRPC calls using app password authentication
+3
Cargo.lock
··· 280 280 "atproto-identity", 281 281 "base64", 282 282 "chrono", 283 + "cid", 283 284 "clap", 285 + "multihash", 284 286 "serde", 285 287 "serde_ipld_dagcbor", 286 288 "serde_json", 289 + "sha2", 287 290 "thiserror 2.0.12", 288 291 "tokio", 289 292 ]
+53 -1
crates/atproto-client/src/client.rs
··· 397 397 Ok(value) 398 398 } 399 399 400 - 400 + /// Performs a DPoP-authenticated HTTP POST request with raw bytes body and additional headers, and parses the response as JSON. 401 + /// 402 + /// This function is similar to `post_dpop_json_with_headers` but accepts a raw bytes payload 403 + /// instead of JSON. Useful for sending pre-serialized data or binary payloads while maintaining 404 + /// DPoP authentication and custom headers. 405 + /// 406 + /// # Arguments 407 + /// 408 + /// * `http_client` - The HTTP client to use for the request 409 + /// * `dpop_auth` - DPoP authentication credentials 410 + /// * `url` - The URL to request 411 + /// * `payload` - The raw bytes to send in the request body 412 + /// * `additional_headers` - Additional HTTP headers to include in the request 413 + /// 414 + /// # Returns 415 + /// 416 + /// The parsed JSON response as a `serde_json::Value` 417 + /// 418 + /// # Errors 419 + /// 420 + /// Returns `DPoPError::ProofGenerationFailed` if DPoP proof generation fails, 421 + /// `DPoPError::HttpRequestFailed` if the HTTP request fails, 422 + /// or `DPoPError::JsonParseFailed` if JSON parsing fails. 423 + /// 424 + /// # Example 425 + /// 426 + /// ```no_run 427 + /// use atproto_client::client::{DPoPAuth, post_dpop_bytes_with_headers}; 428 + /// use atproto_identity::key::identify_key; 429 + /// use reqwest::{Client, header::{HeaderMap, CONTENT_TYPE}}; 430 + /// use bytes::Bytes; 431 + /// 432 + /// # async fn example() -> anyhow::Result<()> { 433 + /// let client = Client::new(); 434 + /// let dpop_auth = DPoPAuth { 435 + /// dpop_private_key_data: identify_key("did:key:zQ3sh...")?, 436 + /// oauth_access_token: "access_token".to_string(), 437 + /// }; 438 + /// 439 + /// let mut headers = HeaderMap::new(); 440 + /// headers.insert(CONTENT_TYPE, "application/json".parse()?); 441 + /// 442 + /// let payload = Bytes::from(r#"{"text": "Hello!"}"#); 443 + /// let response = post_dpop_bytes_with_headers( 444 + /// &client, 445 + /// &dpop_auth, 446 + /// "https://pds.example.com/xrpc/com.atproto.repo.createRecord", 447 + /// payload, 448 + /// &headers 449 + /// ).await?; 450 + /// # Ok(()) 451 + /// # } 452 + /// ``` 401 453 pub async fn post_dpop_bytes_with_headers( 402 454 http_client: &reqwest::Client, 403 455 dpop_auth: &DPoPAuth,
+11 -1
crates/atproto-record/Cargo.toml
··· 21 21 doc = true 22 22 required-features = ["clap", "tokio"] 23 23 24 - [[bin]] 24 + [[bin]] 25 25 name = "atproto-record-verify" 26 26 test = false 27 27 bench = false 28 28 doc = true 29 29 required-features = ["clap", "tokio"] 30 30 31 + [[bin]] 32 + name = "atproto-record-cid" 33 + test = false 34 + bench = false 35 + doc = true 36 + required-features = ["clap"] 37 + 31 38 [dependencies] 32 39 atproto-identity.workspace = true 33 40 ··· 41 48 tokio = { workspace = true, optional = true } 42 49 chrono = {version = "0.4.41", default-features = false, features = ["std", "now", "serde"]} 43 50 clap = { workspace = true, optional = true } 51 + cid = "0.11" 52 + multihash = "0.19" 53 + sha2 = { workspace = true } 44 54 45 55 [features] 46 56 default = ["hickory-dns"]
+150
crates/atproto-record/src/bin/atproto-record-cid.rs
··· 1 + //! Command-line tool for generating CIDs from JSON records. 2 + //! 3 + //! This tool reads JSON from stdin, serializes it using IPLD DAG-CBOR format, 4 + //! and outputs the corresponding CID (Content Identifier) using CIDv1 with 5 + //! SHA-256 hashing. This matches the AT Protocol specification for content 6 + //! addressing of records. 7 + //! 8 + //! # AT Protocol CID Format 9 + //! 10 + //! The tool generates CIDs that follow the AT Protocol specification: 11 + //! - **CID Version**: CIDv1 12 + //! - **Codec**: DAG-CBOR (0x71) 13 + //! - **Hash Function**: SHA-256 (0x12) 14 + //! - **Encoding**: Base32 (default for CIDv1) 15 + //! 16 + //! # Example Usage 17 + //! 18 + //! ```bash 19 + //! # Generate CID from a simple JSON object 20 + //! echo '{"text":"Hello, AT Protocol!"}' | cargo run --features clap --bin atproto-record-cid 21 + //! 22 + //! # Generate CID from a file 23 + //! cat post.json | cargo run --features clap --bin atproto-record-cid 24 + //! 25 + //! # Generate CID from a complex record 26 + //! echo '{ 27 + //! "$type": "app.bsky.feed.post", 28 + //! "text": "Hello world", 29 + //! "createdAt": "2025-01-19T10:00:00.000Z" 30 + //! }' | cargo run --features clap --bin atproto-record-cid 31 + //! ``` 32 + //! 33 + //! # Output Format 34 + //! 35 + //! The tool outputs the CID as a single line string in the format: 36 + //! ```text 37 + //! bafyreibjzlvhtyxnhbvvzl3gj4qmg2ufl2jbhh5qr3gvvxlm7ksf3qwxqq 38 + //! ``` 39 + //! 40 + //! # Error Handling 41 + //! 42 + //! The tool will return an error if: 43 + //! - Input is not valid JSON 44 + //! - JSON cannot be serialized to DAG-CBOR 45 + //! - CID generation fails 46 + //! 47 + //! # Technical Details 48 + //! 49 + //! The CID generation process: 50 + //! 1. Read JSON from stdin 51 + //! 2. Parse JSON into serde_json::Value 52 + //! 3. Serialize to DAG-CBOR bytes using serde_ipld_dagcbor 53 + //! 4. Hash the bytes using SHA-256 54 + //! 5. Create CIDv1 with DAG-CBOR codec 55 + //! 6. Output the CID string 56 + 57 + use anyhow::Result; 58 + use atproto_record::errors::CliError; 59 + use cid::Cid; 60 + use clap::Parser; 61 + use multihash::Multihash; 62 + use sha2::{Digest, Sha256}; 63 + use std::io::{self, Read}; 64 + 65 + /// AT Protocol Record CID Generator 66 + #[derive(Parser)] 67 + #[command( 68 + name = "atproto-record-cid", 69 + version, 70 + about = "Generate CID for AT Protocol DAG-CBOR records from JSON", 71 + long_about = " 72 + A command-line tool for generating Content Identifiers (CIDs) from JSON records 73 + using the AT Protocol DAG-CBOR serialization format. 74 + 75 + The tool reads JSON from stdin, serializes it using IPLD DAG-CBOR format, and 76 + outputs the corresponding CID using CIDv1 with SHA-256 hashing. This matches 77 + the AT Protocol specification for content addressing of records. 78 + 79 + CID FORMAT: 80 + Version: CIDv1 81 + Codec: DAG-CBOR (0x71) 82 + Hash: SHA-256 (0x12) 83 + Encoding: Base32 (default for CIDv1) 84 + 85 + EXAMPLES: 86 + # Generate CID from stdin: 87 + echo '{\"text\":\"Hello!\"}' | atproto-record-cid 88 + 89 + # Generate CID from a file: 90 + cat post.json | atproto-record-cid 91 + 92 + # Complex record with AT Protocol fields: 93 + echo '{ 94 + \"$type\": \"app.bsky.feed.post\", 95 + \"text\": \"Hello world\", 96 + \"createdAt\": \"2025-01-19T10:00:00.000Z\" 97 + }' | atproto-record-cid 98 + 99 + OUTPUT: 100 + The tool outputs a single line containing the CID: 101 + bafyreibjzlvhtyxnhbvvzl3gj4qmg2ufl2jbhh5qr3gvvxlm7ksf3qwxqq 102 + 103 + NOTES: 104 + - Input must be valid JSON 105 + - The same JSON input will always produce the same CID 106 + - Field order in JSON objects may affect the CID due to DAG-CBOR serialization 107 + - Special AT Protocol fields like $type, $sig, and $link are preserved 108 + " 109 + )] 110 + struct Args {} 111 + 112 + fn main() -> Result<()> { 113 + let _args = Args::parse(); 114 + 115 + // Read JSON from stdin 116 + let mut stdin_content = String::new(); 117 + io::stdin() 118 + .read_to_string(&mut stdin_content) 119 + .map_err(|_| CliError::StdinReadFailed)?; 120 + 121 + // Parse JSON 122 + let json_value: serde_json::Value = 123 + serde_json::from_str(&stdin_content).map_err(|_| CliError::StdinJsonParseFailed)?; 124 + 125 + // Serialize to DAG-CBOR 126 + let dag_cbor_bytes = serde_ipld_dagcbor::to_vec(&json_value).map_err(|error| { 127 + CliError::RecordSerializationFailed { 128 + error: error.to_string(), 129 + } 130 + })?; 131 + 132 + // Hash the bytes using SHA-256 133 + // Code 0x12 is SHA-256, size 32 bytes 134 + let mut hasher = Sha256::new(); 135 + hasher.update(&dag_cbor_bytes); 136 + let hash_result = hasher.finalize(); 137 + 138 + let multihash = 139 + Multihash::wrap(0x12, &hash_result).map_err(|error| CliError::CidGenerationFailed { 140 + error: error.to_string(), 141 + })?; 142 + 143 + // Create CIDv1 with DAG-CBOR codec (0x71) 144 + let cid = Cid::new_v1(0x71, multihash); 145 + 146 + // Output the CID 147 + println!("{}", cid); 148 + 149 + Ok(()) 150 + }
+14
crates/atproto-record/src/errors.rs
··· 277 277 /// The name of the missing value 278 278 name: String, 279 279 }, 280 + 281 + /// Occurs when record serialization to DAG-CBOR fails 282 + #[error("error-atproto-record-cli-9 Failed to serialize record to DAG-CBOR: {error}")] 283 + RecordSerializationFailed { 284 + /// The underlying serialization error 285 + error: String, 286 + }, 287 + 288 + /// Occurs when CID generation fails 289 + #[error("error-atproto-record-cli-10 Failed to generate CID: {error}")] 290 + CidGenerationFailed { 291 + /// The underlying CID generation error 292 + error: String, 293 + }, 280 294 }