+3
-1
CLAUDE.md
+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
+3
Cargo.lock
+53
-1
crates/atproto-client/src/client.rs
+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
+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
+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
+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
}