auths_cli/commands/verify_helpers.rs
1use anyhow::{Context, Result, anyhow};
2
3/// The project's pinned trusted roots, read from `<git-toplevel>/.auths/roots` — the
4/// committed trust declaration seeded by `auths init`. Empty when run outside a repo or
5/// when nothing is pinned (so every commit is `RootNotPinned` until a root is pinned).
6///
7/// Usage:
8/// ```ignore
9/// let roots = load_project_pinned_roots();
10/// ```
11pub fn load_project_pinned_roots() -> Vec<String> {
12 let Ok(output) = crate::subprocess::git_command(&["rev-parse", "--show-toplevel"]).output()
13 else {
14 return Vec::new();
15 };
16 if !output.status.success() {
17 return Vec::new();
18 }
19 let root = std::path::PathBuf::from(String::from_utf8_lossy(&output.stdout).trim());
20 auths_sdk::workflows::roots::load_pinned_roots(
21 &crate::adapters::config_store::FileConfigStore,
22 &root.join(".auths"),
23 )
24 .unwrap_or_default()
25}
26
27/// The committed identity bundle, when the repository carries one.
28///
29/// A repo that commits `.auths/ci-bundle.json` (produced by
30/// `auths id export-bundle`) ships its signer's KEL with the code, so a fresh
31/// clone verifies with no flags and no network. The bundle stays evidence-only:
32/// its root must still match an independently pinned root (`.auths/roots` or
33/// self-trust), exactly as with an explicit `--identity-bundle`, which always
34/// wins over discovery.
35///
36/// Usage:
37/// ```ignore
38/// let bundle = cmd.identity_bundle.clone().or_else(discover_project_bundle);
39/// ```
40pub fn discover_project_bundle() -> Option<std::path::PathBuf> {
41 let output = crate::subprocess::git_command(&["rev-parse", "--show-toplevel"])
42 .output()
43 .ok()?;
44 if !output.status.success() {
45 return None;
46 }
47 let root = std::path::PathBuf::from(String::from_utf8_lossy(&output.stdout).trim());
48 let bundle = root.join(".auths").join("ci-bundle.json");
49 bundle.is_file().then_some(bundle)
50}
51
52/// The human-readable label for a surfaced freshness verdict (ADR 009).
53///
54/// Shared by every verify command so an offline verdict renders "freshness unknown"
55/// identically across the CLI — never a bare success that reads as real-time fresh.
56///
57/// Args:
58/// * `freshness`: the verdict's classified freshness.
59///
60/// Usage:
61/// ```ignore
62/// let label = freshness_label(report.freshness());
63/// ```
64pub fn freshness_label(freshness: auths_verifier::Freshness) -> &'static str {
65 match freshness {
66 auths_verifier::Freshness::Fresh => "fresh",
67 auths_verifier::Freshness::Unknown => "unknown",
68 auths_verifier::Freshness::Stale => "stale",
69 }
70}
71
72/// Parse witness key arguments ("did:key:z6Mk...:abcd1234...") into (DID, pk_bytes) tuples.
73pub fn parse_witness_keys(keys: &[String]) -> Result<Vec<(String, Vec<u8>)>> {
74 keys.iter()
75 .map(|s| {
76 // Find the last ':' to split DID from hex key
77 let last_colon = s
78 .rfind(':')
79 .ok_or_else(|| anyhow!("Invalid witness key format '{}': expected format: <did>:<public_key_hex> (e.g. did:key:z6Mk...:abcd1234)", s))?;
80 let did = &s[..last_colon];
81 let pk_hex = &s[last_colon + 1..];
82 let pk_bytes = hex::decode(pk_hex)
83 .with_context(|| format!("Invalid hex in witness key for '{}'", did))?;
84 Ok((did.to_string(), pk_bytes))
85 })
86 .collect()
87}
88
89/// The registry URL to use, or an actionable error when none is configured.
90///
91/// There is no default registry: auths is offline-first, and the one that used to
92/// be baked in (`https://registry.auths.dev`) returned HTTP 404 — a default that
93/// looks configured and fails at the network. Registry-dependent verbs resolve
94/// through here so the refusal is stated once, at the CLI boundary, in terms of
95/// what the user can do about it.
96///
97/// Args:
98/// * `configured`: The value of `--registry` / `AUTHS_REGISTRY_URL`, if any.
99///
100/// Usage:
101/// ```ignore
102/// let registry = require_registry(cmd.registry.clone())?;
103/// ```
104pub fn require_registry(configured: Option<String>) -> Result<String> {
105 configured.ok_or_else(|| {
106 anyhow!(
107 "{}",
108 auths_sdk::domains::identity::error::RegistrationError::NoRegistryConfigured
109 )
110 })
111}