···397397 Ok(value)
398398}
399399400400-400400+/// Performs a DPoP-authenticated HTTP POST request with raw bytes body and additional headers, and parses the response as JSON.
401401+///
402402+/// This function is similar to `post_dpop_json_with_headers` but accepts a raw bytes payload
403403+/// instead of JSON. Useful for sending pre-serialized data or binary payloads while maintaining
404404+/// DPoP authentication and custom headers.
405405+///
406406+/// # Arguments
407407+///
408408+/// * `http_client` - The HTTP client to use for the request
409409+/// * `dpop_auth` - DPoP authentication credentials
410410+/// * `url` - The URL to request
411411+/// * `payload` - The raw bytes to send in the request body
412412+/// * `additional_headers` - Additional HTTP headers to include in the request
413413+///
414414+/// # Returns
415415+///
416416+/// The parsed JSON response as a `serde_json::Value`
417417+///
418418+/// # Errors
419419+///
420420+/// Returns `DPoPError::ProofGenerationFailed` if DPoP proof generation fails,
421421+/// `DPoPError::HttpRequestFailed` if the HTTP request fails,
422422+/// or `DPoPError::JsonParseFailed` if JSON parsing fails.
423423+///
424424+/// # Example
425425+///
426426+/// ```no_run
427427+/// use atproto_client::client::{DPoPAuth, post_dpop_bytes_with_headers};
428428+/// use atproto_identity::key::identify_key;
429429+/// use reqwest::{Client, header::{HeaderMap, CONTENT_TYPE}};
430430+/// use bytes::Bytes;
431431+///
432432+/// # async fn example() -> anyhow::Result<()> {
433433+/// let client = Client::new();
434434+/// let dpop_auth = DPoPAuth {
435435+/// dpop_private_key_data: identify_key("did:key:zQ3sh...")?,
436436+/// oauth_access_token: "access_token".to_string(),
437437+/// };
438438+///
439439+/// let mut headers = HeaderMap::new();
440440+/// headers.insert(CONTENT_TYPE, "application/json".parse()?);
441441+///
442442+/// let payload = Bytes::from(r#"{"text": "Hello!"}"#);
443443+/// let response = post_dpop_bytes_with_headers(
444444+/// &client,
445445+/// &dpop_auth,
446446+/// "https://pds.example.com/xrpc/com.atproto.repo.createRecord",
447447+/// payload,
448448+/// &headers
449449+/// ).await?;
450450+/// # Ok(())
451451+/// # }
452452+/// ```
401453pub async fn post_dpop_bytes_with_headers(
402454 http_client: &reqwest::Client,
403455 dpop_auth: &DPoPAuth,
···11+//! Command-line tool for generating CIDs from JSON records.
22+//!
33+//! This tool reads JSON from stdin, serializes it using IPLD DAG-CBOR format,
44+//! and outputs the corresponding CID (Content Identifier) using CIDv1 with
55+//! SHA-256 hashing. This matches the AT Protocol specification for content
66+//! addressing of records.
77+//!
88+//! # AT Protocol CID Format
99+//!
1010+//! The tool generates CIDs that follow the AT Protocol specification:
1111+//! - **CID Version**: CIDv1
1212+//! - **Codec**: DAG-CBOR (0x71)
1313+//! - **Hash Function**: SHA-256 (0x12)
1414+//! - **Encoding**: Base32 (default for CIDv1)
1515+//!
1616+//! # Example Usage
1717+//!
1818+//! ```bash
1919+//! # Generate CID from a simple JSON object
2020+//! echo '{"text":"Hello, AT Protocol!"}' | cargo run --features clap --bin atproto-record-cid
2121+//!
2222+//! # Generate CID from a file
2323+//! cat post.json | cargo run --features clap --bin atproto-record-cid
2424+//!
2525+//! # Generate CID from a complex record
2626+//! echo '{
2727+//! "$type": "app.bsky.feed.post",
2828+//! "text": "Hello world",
2929+//! "createdAt": "2025-01-19T10:00:00.000Z"
3030+//! }' | cargo run --features clap --bin atproto-record-cid
3131+//! ```
3232+//!
3333+//! # Output Format
3434+//!
3535+//! The tool outputs the CID as a single line string in the format:
3636+//! ```text
3737+//! bafyreibjzlvhtyxnhbvvzl3gj4qmg2ufl2jbhh5qr3gvvxlm7ksf3qwxqq
3838+//! ```
3939+//!
4040+//! # Error Handling
4141+//!
4242+//! The tool will return an error if:
4343+//! - Input is not valid JSON
4444+//! - JSON cannot be serialized to DAG-CBOR
4545+//! - CID generation fails
4646+//!
4747+//! # Technical Details
4848+//!
4949+//! The CID generation process:
5050+//! 1. Read JSON from stdin
5151+//! 2. Parse JSON into serde_json::Value
5252+//! 3. Serialize to DAG-CBOR bytes using serde_ipld_dagcbor
5353+//! 4. Hash the bytes using SHA-256
5454+//! 5. Create CIDv1 with DAG-CBOR codec
5555+//! 6. Output the CID string
5656+5757+use anyhow::Result;
5858+use atproto_record::errors::CliError;
5959+use cid::Cid;
6060+use clap::Parser;
6161+use multihash::Multihash;
6262+use sha2::{Digest, Sha256};
6363+use std::io::{self, Read};
6464+6565+/// AT Protocol Record CID Generator
6666+#[derive(Parser)]
6767+#[command(
6868+ name = "atproto-record-cid",
6969+ version,
7070+ about = "Generate CID for AT Protocol DAG-CBOR records from JSON",
7171+ long_about = "
7272+A command-line tool for generating Content Identifiers (CIDs) from JSON records
7373+using the AT Protocol DAG-CBOR serialization format.
7474+7575+The tool reads JSON from stdin, serializes it using IPLD DAG-CBOR format, and
7676+outputs the corresponding CID using CIDv1 with SHA-256 hashing. This matches
7777+the AT Protocol specification for content addressing of records.
7878+7979+CID FORMAT:
8080+ Version: CIDv1
8181+ Codec: DAG-CBOR (0x71)
8282+ Hash: SHA-256 (0x12)
8383+ Encoding: Base32 (default for CIDv1)
8484+8585+EXAMPLES:
8686+ # Generate CID from stdin:
8787+ echo '{\"text\":\"Hello!\"}' | atproto-record-cid
8888+8989+ # Generate CID from a file:
9090+ cat post.json | atproto-record-cid
9191+9292+ # Complex record with AT Protocol fields:
9393+ echo '{
9494+ \"$type\": \"app.bsky.feed.post\",
9595+ \"text\": \"Hello world\",
9696+ \"createdAt\": \"2025-01-19T10:00:00.000Z\"
9797+ }' | atproto-record-cid
9898+9999+OUTPUT:
100100+ The tool outputs a single line containing the CID:
101101+ bafyreibjzlvhtyxnhbvvzl3gj4qmg2ufl2jbhh5qr3gvvxlm7ksf3qwxqq
102102+103103+NOTES:
104104+ - Input must be valid JSON
105105+ - The same JSON input will always produce the same CID
106106+ - Field order in JSON objects may affect the CID due to DAG-CBOR serialization
107107+ - Special AT Protocol fields like $type, $sig, and $link are preserved
108108+"
109109+)]
110110+struct Args {}
111111+112112+fn main() -> Result<()> {
113113+ let _args = Args::parse();
114114+115115+ // Read JSON from stdin
116116+ let mut stdin_content = String::new();
117117+ io::stdin()
118118+ .read_to_string(&mut stdin_content)
119119+ .map_err(|_| CliError::StdinReadFailed)?;
120120+121121+ // Parse JSON
122122+ let json_value: serde_json::Value =
123123+ serde_json::from_str(&stdin_content).map_err(|_| CliError::StdinJsonParseFailed)?;
124124+125125+ // Serialize to DAG-CBOR
126126+ let dag_cbor_bytes = serde_ipld_dagcbor::to_vec(&json_value).map_err(|error| {
127127+ CliError::RecordSerializationFailed {
128128+ error: error.to_string(),
129129+ }
130130+ })?;
131131+132132+ // Hash the bytes using SHA-256
133133+ // Code 0x12 is SHA-256, size 32 bytes
134134+ let mut hasher = Sha256::new();
135135+ hasher.update(&dag_cbor_bytes);
136136+ let hash_result = hasher.finalize();
137137+138138+ let multihash =
139139+ Multihash::wrap(0x12, &hash_result).map_err(|error| CliError::CidGenerationFailed {
140140+ error: error.to_string(),
141141+ })?;
142142+143143+ // Create CIDv1 with DAG-CBOR codec (0x71)
144144+ let cid = Cid::new_v1(0x71, multihash);
145145+146146+ // Output the CID
147147+ println!("{}", cid);
148148+149149+ Ok(())
150150+}
+14
crates/atproto-record/src/errors.rs
···277277 /// The name of the missing value
278278 name: String,
279279 },
280280+281281+ /// Occurs when record serialization to DAG-CBOR fails
282282+ #[error("error-atproto-record-cli-9 Failed to serialize record to DAG-CBOR: {error}")]
283283+ RecordSerializationFailed {
284284+ /// The underlying serialization error
285285+ error: String,
286286+ },
287287+288288+ /// Occurs when CID generation fails
289289+ #[error("error-atproto-record-cli-10 Failed to generate CID: {error}")]
290290+ CidGenerationFailed {
291291+ /// The underlying CID generation error
292292+ error: String,
293293+ },
280294}