Skip to main content

auths_cli/commands/
registry.rs

1//! Registry propagation commands: push/pull `refs/auths/registry` over a git
2//! remote, so a second machine sees this machine's identity events (rotations,
3//! revocations, new devices) over a wire instead of a shared filesystem.
4
5use std::path::PathBuf;
6
7use anyhow::{Result, anyhow};
8use clap::{Parser, Subcommand};
9use serde::Serialize;
10
11use auths_sdk::storage::{
12    MergeOutcome, MergedCredentials, MergedKel, PushOutcome, pull_registry, push_registry,
13};
14
15use crate::commands::executable::ExecutableCommand;
16use crate::config::CliConfig;
17use crate::ux::format::{JsonResponse, Output, is_json_mode};
18
19/// Sync the local identity registry with a git remote.
20#[derive(Parser, Debug, Clone)]
21#[command(
22    name = "registry",
23    about = "Push/pull the identity registry to/from a git remote",
24    after_help = "The registry travels as the packed git ref refs/auths/registry, so any
25git remote (a bare repo over file://, git://, ssh://, https://) can carry it
26between machines. Push is fast-forward-only (the registry is append-only).
27Pull authenticates every fetched KEL (prefix binding, signature replay,
28fork refusal) before merging it into the local registry.
29
30Examples:
31  auths registry push git://wire.local/registry.git   # publish this machine's registry
32  auths registry pull git://wire.local/registry.git   # merge another machine's events
33
34Related:
35  auths id show      — The identity whose events the registry carries
36  auths device list  — Devices learned from merged registries"
37)]
38pub struct RegistryCommand {
39    #[command(subcommand)]
40    pub subcommand: RegistrySubcommand,
41}
42
43/// Registry sync subcommands.
44#[derive(Subcommand, Debug, Clone)]
45pub enum RegistrySubcommand {
46    /// Publish the local registry ref to a git remote (fast-forward only).
47    Push {
48        /// Git remote URL (file://, git://, ssh://, https://).
49        #[arg(value_name = "REMOTE")]
50        remote: String,
51    },
52    /// Fetch a remote registry and merge its authenticated KELs locally.
53    Pull {
54        /// Git remote URL (file://, git://, ssh://, https://).
55        #[arg(value_name = "REMOTE")]
56        remote: String,
57    },
58}
59
60#[derive(Debug, Serialize)]
61struct PushReport {
62    remote: String,
63    outcome: PushOutcome,
64}
65
66#[derive(Debug, Serialize)]
67struct PullReport {
68    remote: String,
69    merged: Vec<MergedKel>,
70    #[serde(flatten)]
71    credentials: MergedCredentials,
72}
73
74impl ExecutableCommand for RegistryCommand {
75    fn execute(&self, ctx: &CliConfig) -> Result<()> {
76        let registry_root = resolve_registry_root(ctx)?;
77        match &self.subcommand {
78            RegistrySubcommand::Push { remote } => {
79                let outcome = push_registry(&registry_root, remote)?;
80                if is_json_mode() {
81                    JsonResponse::success(
82                        "registry push",
83                        PushReport {
84                            remote: remote.clone(),
85                            outcome,
86                        },
87                    )
88                    .print()?;
89                } else {
90                    let out = Output::new();
91                    match outcome {
92                        PushOutcome::Updated => {
93                            out.print_success(&format!("Registry pushed to {remote}"));
94                        }
95                        PushOutcome::AlreadyCurrent => {
96                            out.print_info(&format!("Remote {remote} is already current"));
97                        }
98                    }
99                }
100                Ok(())
101            }
102            RegistrySubcommand::Pull { remote } => {
103                let report = pull_registry(&registry_root, remote)?;
104                if is_json_mode() {
105                    JsonResponse::success(
106                        "registry pull",
107                        PullReport {
108                            remote: remote.clone(),
109                            merged: report.merged,
110                            credentials: report.credentials,
111                        },
112                    )
113                    .print()?;
114                } else {
115                    let out = Output::new();
116                    out.print_success(&format!("Registry pulled from {remote}"));
117                    for kel in &report.merged {
118                        let line = match &kel.outcome {
119                            MergeOutcome::Imported { events } => {
120                                format!("{} imported ({events} events)", kel.prefix)
121                            }
122                            MergeOutcome::Advanced { events } => {
123                                format!("{} advanced (+{events} events)", kel.prefix)
124                            }
125                            MergeOutcome::AlreadyCurrent => {
126                                format!("{} already current", kel.prefix)
127                            }
128                        };
129                        out.print_info(&line);
130                    }
131                    let creds = &report.credentials;
132                    if creds.credentials_imported > 0 || creds.tel_events_imported > 0 {
133                        out.print_info(&format!(
134                            "{} credential(s) and {} status event(s) imported",
135                            creds.credentials_imported, creds.tel_events_imported
136                        ));
137                    }
138                }
139                Ok(())
140            }
141        }
142    }
143}
144
145/// The registry root the sync operates on: the global `--repo` override when
146/// given, otherwise the configured Auths home.
147fn resolve_registry_root(ctx: &CliConfig) -> Result<PathBuf> {
148    if let Some(repo) = &ctx.repo_path {
149        return Ok(repo.clone());
150    }
151    auths_sdk::paths::auths_home_with_config(&ctx.env_config)
152        .map_err(|e| anyhow!("Could not locate ~/.auths: {e}"))
153}