use crate::audit::AuditArgs;
use crate::{custody, CliError, CliResult};
use clap::Args;
use sha2::{Digest, Sha256};
use std::io::Write;
use std::path::{Path, PathBuf};
#[derive(Debug, Args)]
pub struct RevealArgs {
#[arg(
long,
default_value = "outputs",
help = "runtime outputs root where the reveal proof's audit.md is appended"
)]
pub root: PathBuf,
#[arg(
long,
help = "live DB path holding the custody vault; defaults to the cwd .zynk/zynk.db (reveal needs the DB — there is no file-only reveal)"
)]
pub db: Option<PathBuf>,
#[arg(
long,
help = "path to the operator-owned custody key file (default <db-dir>/custody.key or $ZYNK_CUSTODY_KEY_FILE)"
)]
pub custody_key_file: Option<PathBuf>,
#[arg(
long,
default_value = "operator",
help = "actor recorded as the source/observer of the reveal proof"
)]
pub actor: String,
#[arg(long)]
pub timestamp: Option<String>,
#[arg(help = "the audit_id of the retained record to reveal")]
pub audit_id: String,
}
pub fn run(args: RevealArgs) -> CliResult<()> {
let db_path = crate::db::resolve_projection_target(args.db.as_deref(), false)
.into_path_and_mode()
.map(|(path, _explicit)| path)
.ok_or_else(|| CliError::failure("reveal needs the live DB"))?;
if !db_path.exists() {
return Err(CliError::failure(format!(
"reveal needs the live DB, but none was found at {} (pass --db <path>)",
db_path.display()
)));
}
let conn = crate::db::open_read_database(&db_path)?;
let vault = crate::db::read_custody_vault(&conn, &args.audit_id)?.ok_or_else(|| {
CliError::usage(format!(
"{} is not revealable (no retained custody)",
args.audit_id
))
})?;
if vault.cipher_id != custody::CUSTODY_CIPHER_ID
|| vault.key_version != custody::CUSTODY_KEY_VERSION
{
return Err(CliError::failure(format!(
"{} uses unsupported custody cipher/version ({}/{})",
args.audit_id, vault.cipher_id, vault.key_version
)));
}
let (session_id, payload_hash) = crate::db::read_reveal_target(&conn, &args.audit_id)?
.ok_or_else(|| CliError::failure(format!("no audit record for {}", args.audit_id)))?;
let key_path = custody::resolve_key_path(args.custody_key_file.as_deref(), &db_path);
let key = custody::load_existing_key(&key_path)?;
let plaintext = custody::decrypt(
&key,
&args.audit_id,
&payload_hash,
&vault.nonce,
&vault.ciphertext,
)?;
let recomputed = format!("sha256:{:x}", Sha256::digest(&plaintext));
if recomputed != payload_hash {
return Err(CliError::failure(format!(
"{} reveal aborted: decrypted plaintext hash does not match the recorded record",
args.audit_id
)));
}
write_reveal_proof(&args, &db_path, &session_id)?;
std::io::stdout()
.write_all(&plaintext)
.map_err(|error| CliError::failure(format!("failed to write plaintext: {error}")))?;
Ok(())
}
fn write_reveal_proof(args: &RevealArgs, db_path: &Path, session_id: &str) -> CliResult<()> {
let proof = AuditArgs {
profile: None,
root: args.root.clone(),
session_id: session_id.to_string(),
audit_id: None,
previous_audit_id: None,
timestamp: args.timestamp.clone(),
due: None,
source_agent: args.actor.clone(),
source_address: "cli".to_string(),
target_agent: "none".to_string(),
target_address: "none".to_string(),
transport: "none".to_string(),
workspace_id: "none".to_string(),
transport_thread_id: None,
mid: format!("reveal-{}", args.audit_id),
record_type: "reveal".to_string(),
mode: None,
r#ref: Some(args.audit_id.clone()),
re: None,
command_origin: "operator".to_string(),
payload: Some(format!("revealed {}", args.audit_id)),
payload_file: None,
payload_redaction_policy: "full".to_string(),
payload_ref: None,
sensitive_category: None,
excerpt_chars: 12,
delivery_status: "observed".to_string(),
observed_by: args.actor.clone(),
verified_by: "operator".to_string(),
db: Some(db_path.to_path_buf()),
no_db: false,
retain_custody: false,
custody_key_file: None,
};
let profile = crate::profile::load_profile(proof.profile.as_deref())?;
crate::audit::validate_audit_args(&proof, &profile)?;
let (_audit_path, record) = crate::audit::write_audit_file(&proof, &profile)?;
crate::audit::project_record(proof.db.as_deref(), proof.no_db, &proof.root, &record)
}