| 09752034 Andy McCall ↶ |
1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
use super::{ui, Ctx}; |
| 6 |
use anyhow::{bail, Context, Result}; |
| 7 |
use oxy_core::identity::Identity; |
| 8 |
use spectra_core::keys::KeyPair; |
| 9 |
use spectra_core::ledger::Ledger; |
| 10 |
use spectra_core::principals::{identity_payload, signed_event, Principals}; |
| 11 |
use std::path::{Path, PathBuf}; |
| 12 |
|
| 13 |
|
| 14 |
|
| 15 |
pub fn owner_home(ctx: &Ctx, flag: Option<PathBuf>) -> PathBuf { |
| 16 |
flag.or_else(|| ctx.config_owner_home()).unwrap_or_else(oxy_core::repo::default_home) |
| 17 |
} |
| 18 |
|
| 19 |
pub async fn init(ctx: &Ctx, owner_home_flag: Option<PathBuf>, name: Option<String>) -> Result<i32> { |
| 20 |
let data = &ctx.data; |
| 21 |
let db = data.join("spectra.db"); |
| 22 |
if db.exists() { |
| 23 |
bail!("{} already holds a Spectra (spectra identity show)", data.display()); |
| 24 |
} |
| 25 |
let home = owner_home(ctx, owner_home_flag); |
| 26 |
let owner = KeyPair::load_home(&home).with_context(|| { |
| 27 |
format!("the owner signs with their own identity: none usable in {} (oxy identity create, or --owner-home)", home.display()) |
| 28 |
})?; |
| 29 |
std::fs::create_dir_all(data)?; |
| 30 |
#[cfg(unix)] |
| 31 |
{ |
| 32 |
use std::os::unix::fs::PermissionsExt; |
| 33 |
std::fs::set_permissions(data, std::fs::Permissions::from_mode(0o700))?; |
| 34 |
} |
| 35 |
let name = name.unwrap_or_else(hostname); |
| 36 |
let instance = KeyPair::instance(data, &name)?; |
| 37 |
let ledger = Ledger::open(&db, Some(instance.clone())).await?; |
| 5eb57578 Andy McCall ↶ |
38 |
let mut first = vec![ |
| 39 |
signed_event(&owner, "human", "identity.owner", identity_payload(&owner.identity)), |
| 40 |
signed_event(&instance, "system", "identity.instance", identity_payload(&instance.identity)), |
| 41 |
]; |
| 42 |
first.extend(spectra_core::conversation::default_channel_events()); |
| 43 |
ledger.append(first).await?; |
| 09752034 Andy McCall ↶ |
44 |
ledger.checkpoint().await?; |
| 45 |
let home_s = home.display().to_string(); |
| 46 |
std::fs::write( |
| 47 |
data.join("spectra.toml"), |
| 48 |
format!("# Spectra's configuration (spec §22.1).\n\n[owner]\nkey = \"{}\"\nhome = {}\n", owner.key.to_hex(), toml_str(&home_s)), |
| 49 |
)?; |
| 5d695d86 Andy McCall ↶ |
50 |
println!("{} {} {}", ui::done("Initialised"), ui::subject("Spectra"), ui::path(data.display())); |
| 51 |
println!("{}", ui::kv("owner", format!("{}{}{}", ui::author(&owner.identity.name), ui::sep(), ui::hash(owner.key.short(12))))); |
| 52 |
println!("{}", ui::kv("instance", format!("{}{}{}", name, ui::sep(), ui::hash(instance.key.short(12))))); |
| 53 |
println!("{}", ui::kv("ledger", format!("{}{}", ui::path(db.display()), ui::dim(" (verified with spectra admin verify)")))); |
| 09752034 Andy McCall ↶ |
54 |
println!(); |
| 5d695d86 Andy McCall ↶ |
55 |
println!(" {} {}", ui::note(), ui::dim("so that checkpoints are verifiably from your machine, delegate to the instance key:")); |
| 09752034 Andy McCall ↶ |
56 |
println!( |
| 5d695d86 Andy McCall ↶ |
57 |
" {}", |
| 09752034 Andy McCall ↶ |
58 |
ui::id(format!("oxy identity delegate add {} --scope changes", data.join("keys").join("instance.identity").display())) |
| 59 |
); |
| 5d695d86 Andy McCall ↶ |
60 |
println!( |
| 61 |
" {} {} {}", |
| 62 |
ui::note(), |
| 63 |
ui::dim("then add agents with"), |
| 64 |
ui::id("spectra agent add NAME --kind KIND --project PROJECT --home DIR") |
| 65 |
); |
| 09752034 Andy McCall ↶ |
66 |
Ok(0) |
| 67 |
} |
| 68 |
|
| 69 |
pub async fn show(ctx: &Ctx, json: bool) -> Result<i32> { |
| 70 |
let ledger = Ledger::open_read_only(&ctx.db()?).await?; |
| 71 |
let p = Principals::load(&ledger).await?; |
| 72 |
let owner = p.owner().context("no owner is recorded")?; |
| 73 |
let instance = p.instance(); |
| 74 |
let delegated = |
| 75 |
instance.is_some_and(|i| owner.delegates_to(&i.hash(), spectra_core::principals::DELEGATION_SCOPE, spectra_core::ids::now_ms())); |
| 76 |
if json { |
| 77 |
println!( |
| 78 |
"{}", |
| 79 |
serde_json::json!({ |
| 80 |
"owner": { "name": owner.name, "key": owner.hash().to_hex() }, |
| 81 |
"instance": instance.map(|i| serde_json::json!({ "name": i.name, "key": i.hash().to_hex(), "delegated": delegated })), |
| 82 |
}) |
| 83 |
); |
| 84 |
return Ok(0); |
| 85 |
} |
| 5d695d86 Andy McCall ↶ |
86 |
println!("{}", ui::section("Owner")); |
| 87 |
println!("{}", ui::kv("name", ui::author(format!("{} <{}>", owner.name, owner.email)))); |
| 88 |
println!("{}", ui::kv("key", ui::hash(owner.hash().to_hex()))); |
| 89 |
println!("{}", ui::kv("delegates", owner.delegates.len())); |
| 09752034 Andy McCall ↶ |
90 |
if let Some(i) = instance { |
| 5d695d86 Andy McCall ↶ |
91 |
println!("{}", ui::section("Instance")); |
| 92 |
println!("{}", ui::kv("name", &i.name)); |
| 93 |
println!("{}", ui::kv("key", ui::hash(i.hash().to_hex()))); |
| 94 |
if delegated { |
| 95 |
println!("{}", ui::kv("status", ui::ok("delegated by the owner: checkpoints are verifiably yours"))); |
| 96 |
} else { |
| 97 |
println!("{}", ui::kv("status", ui::warn("not delegated by the owner yet"))); |
| 98 |
println!( |
| 99 |
" {} {}", |
| 100 |
ui::note(), |
| 101 |
ui::id(format!("oxy identity delegate add {} --scope changes", ctx.data.join("keys").join("instance.identity").display())) |
| 102 |
); |
| 103 |
} |
| 09752034 Andy McCall ↶ |
104 |
} |
| 105 |
Ok(0) |
| 106 |
} |
| 107 |
|
| 108 |
|
| 109 |
|
| 110 |
pub async fn refresh(ctx: &Ctx, owner_home_flag: Option<PathBuf>) -> Result<i32> { |
| 571572f5 Andy McCall ↶ |
111 |
let home = owner_home(ctx, owner_home_flag); |
| 112 |
let w = ctx.owner_writer(&home).await?; |
| 113 |
let rotated = refresh_event(w.ledger(), &home).await?; |
| 114 |
let outcome = rotated.as_ref().map(|_| KeyPair::load_home(&home).map(|p| p.identity)).transpose()?; |
| 115 |
if let Some(e) = rotated { |
| 116 |
w.append(vec![e]).await?; |
| 117 |
} |
| 09752034 Andy McCall ↶ |
118 |
match outcome { |
| 5d695d86 Andy McCall ↶ |
119 |
Some(id) => println!( |
| 120 |
"{} {}{}{}", |
| 121 |
ui::done("Recorded"), |
| 122 |
ui::subject("the owner's current identity"), |
| 123 |
ui::sep(), |
| 124 |
ui::hash(id.hash().short(12)) |
| 125 |
), |
| 126 |
None => println!("{} {}", ui::done("Current"), ui::subject("the owner's identity has not moved")), |
| 09752034 Andy McCall ↶ |
127 |
} |
| 128 |
Ok(0) |
| 129 |
} |
| 130 |
|
| 571572f5 Andy McCall ↶ |
131 |
|
| 132 |
|
| 133 |
|
| 134 |
pub async fn refresh_event(ledger: &Ledger, home: &Path) -> Result<Option<spectra_core::event::NewEvent>> { |
| 09752034 Andy McCall ↶ |
135 |
let p = Principals::load(ledger).await?; |
| 136 |
let recorded = p.owner().context("no owner is recorded (spectra init)")?.clone(); |
| 137 |
let pair = KeyPair::load_home(home)?; |
| 138 |
if pair.key == recorded.hash() { |
| 139 |
return Ok(None); |
| 140 |
} |
| 141 |
let mut chain = Vec::new(); |
| 142 |
let mut at = pair.identity.clone(); |
| 143 |
while let Some(prev) = at.previous { |
| 144 |
if prev == recorded.hash() { |
| 145 |
break; |
| 146 |
} |
| 147 |
let file = home.join("keys").join(format!("{}.identity", prev.to_hex())); |
| 148 |
at = Identity::decode( |
| 149 |
&std::fs::read(&file).with_context(|| format!("{} is not a successor of the recorded owner", pair.identity.name))?, |
| 150 |
) |
| 151 |
.map_err(|e| anyhow::anyhow!("{}: {e}", file.display()))?; |
| 152 |
chain.push(hex::encode(at.encode())); |
| 153 |
if chain.len() > 1000 { |
| 154 |
bail!("the owner's identity chain is too long to follow"); |
| 155 |
} |
| 156 |
} |
| 157 |
if at.previous != Some(recorded.hash()) && pair.identity.previous != Some(recorded.hash()) { |
| 158 |
bail!("{} in {} is not a successor of the recorded owner {}", pair.identity.name, home.display(), recorded.hash().short(12)); |
| 159 |
} |
| 160 |
let mut payload = identity_payload(&pair.identity); |
| 161 |
payload["chain"] = chain.into(); |
| 571572f5 Andy McCall ↶ |
162 |
Ok(Some(signed_event(&pair, "human", "identity.rotated", payload))) |
| 163 |
} |
| 164 |
|
| 165 |
|
| 166 |
|
| 167 |
|
| 168 |
pub async fn recorded_owner(ledger: &Ledger, home: &Path) -> Result<KeyPair> { |
| 169 |
let p = Principals::load(ledger).await?; |
| 170 |
let recorded = p.owner().context("no owner is recorded (spectra init)")?.hash(); |
| 171 |
let keys = home.join("keys"); |
| 172 |
KeyPair::load(&keys.join(format!("{}.secret", recorded.to_hex())), &keys.join(format!("{}.identity", recorded.to_hex()))) |
| 173 |
.with_context(|| format!("the recorded owner's key is not in {}", home.display())) |
| 09752034 Andy McCall ↶ |
174 |
} |
| 175 |
|
| 176 |
fn hostname() -> String { |
| 177 |
std::fs::read_to_string("/etc/hostname") |
| 178 |
.ok() |
| 179 |
.map(|h| h.trim().to_string()) |
| 180 |
.filter(|h| !h.is_empty()) |
| 181 |
.unwrap_or_else(|| "spectra".into()) |
| 182 |
} |
| 183 |
|
| 184 |
fn toml_str(s: &str) -> String { |
| 185 |
serde_json::to_string(s).unwrap_or_default() |
| 186 |
} |