use anyhow::{Result, anyhow};
use atproto_identity::{
key::{KeyData, identify_key},
resolve::{InputType, parse_input},
};
use atproto_record::signature::verify;
use std::{
env, fs,
io::{self, Read},
};
#[tokio::main]
async fn main() -> Result<()> {
let args: Vec<String> = env::args().skip(1).collect();
if args.is_empty() || args.iter().any(|arg| arg == "--help" || arg == "-h") {
println!("AT Protocol Record Verifying Tool");
println!();
println!("USAGE:");
println!(
" atproto-record-verify <ISSUER_DID> <KEY> <RECORD_INPUT> repository=<REPO> collection=<COLLECTION> [key=value...]"
);
println!();
println!("ARGUMENTS:");
println!(" <ISSUER_DID> DID of the issuer (e.g., did:plc:...)");
println!(" <KEY> DID key for verifying (e.g., did:key:z42tv1...)");
println!(
" <RECORD_INPUT> Path to JSON file containing the record to verify, or '--' to read from stdin"
);
println!();
println!("REQUIRED PARAMETERS:");
println!(" repository=<REPO> Repository DID context");
println!(" collection=<COLLECTION> Collection name context");
println!();
println!("EXAMPLES:");
println!(" # Verify from file:");
println!(" atproto-record-verify \\");
println!(" did:plc:tgudj2fjm77pzkuawquqhsxm \\");
println!(" did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \\");
println!(" ./signed_record.json \\");
println!(" repository=did:plc:4zutorghlchjxzgceklue4la \\");
println!(" collection=community.lexicon.badge.award");
println!();
println!(" # Verify from stdin:");
println!(" echo '{{\"signatures\":[...],...}}' | atproto-record-verify \\");
println!(" did:plc:tgudj2fjm77pzkuawquqhsxm \\");
println!(" did:key:z42tv1pb3Dzog28Q1udyieg1YJP3x1Un5vraE1bttXeCDSpW \\");
println!(" -- \\");
println!(" repository=did:plc:4zutorghlchjxzgceklue4la \\");
println!(" collection=app.bsky.feed.post");
return Ok(());
}
let arguments = args.into_iter();
let mut collection: Option<String> = None;
let mut repository: Option<String> = None;
let mut record: Option<serde_json::Value> = None;
let mut issuer: Option<String> = None;
let mut key_data: Option<KeyData> = None;
for argument in arguments {
if let Some((key, value)) = argument.split_once("=") {
match key {
"collection" => {
collection = Some(value.to_string());
}
"repository" => {
repository = Some(value.to_string());
}
_ => {}
}
} else if argument.starts_with("did:key:") {
key_data = Some(identify_key(&argument)?);
} else if argument.starts_with("did:") {
match parse_input(&argument) {
Ok(InputType::Plc(did)) | Ok(InputType::Web(did)) => {
issuer = Some(did);
}
Ok(_) => return Err(anyhow!("Unsupported DID method: {}", argument)),
Err(e) => return Err(anyhow!("Failed to parse DID {}: {}", argument, e)),
}
} else if argument == "--" {
if record.is_none() {
let mut stdin_content = String::new();
io::stdin()
.read_to_string(&mut stdin_content)
.map_err(|e| anyhow!("Failed to read from stdin: {}", e))?;
record = Some(
serde_json::from_str(&stdin_content)
.map_err(|e| anyhow!("Failed to parse JSON from stdin: {}", e))?,
);
} else {
return Err(anyhow!("Unexpected argument: {}", argument));
}
} else {
if record.is_none() {
let file_content = fs::read_to_string(&argument)
.map_err(|e| anyhow!("Failed to read file {}: {}", argument, e))?;
record =
Some(serde_json::from_str(&file_content).map_err(|e| {
anyhow!("Failed to parse JSON from file {}: {}", argument, e)
})?);
} else {
return Err(anyhow!("Unexpected argument: {}", argument));
}
}
}
let collection = collection.ok_or(anyhow!("missing collection value"))?;
let repository = repository.ok_or(anyhow!("missing repository value"))?;
let record = record.ok_or(anyhow!("missing record source value"))?;
let issuer = issuer.ok_or(anyhow!("missing issuer value"))?;
let key_data = key_data.ok_or(anyhow!("missing key data"))?;
verify(&issuer, &key_data, record, &repository, &collection).await?;
println!("OK");
Ok(())
}