use std::io::Write;
use std::path::PathBuf;
use crate::session_account::provision_session;
use crate::{ProviderKind, STORE_ROOT_ENV};
fn fail(msg: &str) -> ! {
eprintln!("m4a-agent account: {msg}");
std::process::exit(64);
}
pub fn run(args: Vec<String>) {
let mut it = args.into_iter();
match it.next().as_deref() {
Some("provision") => {}
Some("check") => return check(it.collect()),
Some("node-enroll") => return node_enroll(it.collect()),
_ => fail("usage: m4a-agent account provision --provider P --session ID [--nick N] --cap-out FILE [--rotate] | account check --nick N"),
}
let (mut provider, mut session, mut nick, mut out, mut rotate) = (None, None, None, None, false);
while let Some(a) = it.next() {
match a.as_str() {
"--provider" => provider = it.next(),
"--session" => session = it.next(),
"--nick" => nick = it.next(),
"--cap-out" => out = it.next().map(PathBuf::from),
"--rotate" => rotate = true,
other => fail(&format!("unknown argument {other}")),
}
}
let provider = provider.as_deref().and_then(ProviderKind::parse).unwrap_or_else(|| fail("--provider is required"));
let session = session.unwrap_or_else(|| fail("--session is required"));
let out = out.unwrap_or_else(|| fail("--cap-out is required"));
let base = std::env::var(crate::ACCOUNTS_ROOT_ENV)
.ok()
.filter(|v| !v.is_empty())
.or_else(|| std::env::var(STORE_ROOT_ENV).ok().filter(|v| !v.is_empty()))
.map(PathBuf::from)
.unwrap_or_else(|| fail("M4A_ACCOUNTS_ROOT (or M4A_STORE_ROOT) is required"));
let made = provision_session(&base, provider, &session, nick.as_deref(), rotate).unwrap_or_else(|e| fail(&e.to_string()));
if let Some(cap) = &made.cap {
let mut o = std::fs::OpenOptions::new();
o.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
o.mode(0o600);
}
let mut f = o.open(&out).unwrap_or_else(|e| fail(&format!("cap-out: {}", e.kind())));
f.write_all(cap.as_bytes()).unwrap_or_else(|e| fail(&format!("cap-out: {}", e.kind())));
}
println!(
"{}",
serde_json::json!({ "nick": made.entry.nick, "account_ref": made.account_ref, "store_root": made.store_root, "cap_issued": made.cap.is_some(), "enrolled": made.entry.state == crate::session_account::ServerState::Enrolled })
);
}
fn check(args: Vec<String>) {
let mut it = args.into_iter();
let mut nick = None;
while let Some(a) = it.next() {
match a.as_str() {
"--nick" => nick = it.next(),
other => fail(&format!("unknown argument {other}")),
}
}
let nick = nick.unwrap_or_else(|| fail("--nick is required"));
let root = std::env::var(STORE_ROOT_ENV).ok().filter(|v| !v.is_empty()).map(PathBuf::from);
let cap = crate::send::session_cap_from_env();
match crate::session_gate(root.as_deref(), &nick, cap.as_deref(), crate::ipc_own_uid()) {
Ok(()) => println!("ok {nick}"),
Err(why) => {
eprintln!("refused: {why}");
std::process::exit(1);
}
}
}
fn node_enroll(args: Vec<String>) {
let mut it = args.into_iter();
let (mut node, mut ts, mut nonce, mut sig, mut nick) = (None, None, None, None, None);
while let Some(a) = it.next() {
match a.as_str() {
"--node-id" => node = it.next(),
"--ts" => ts = it.next().and_then(|v| v.parse::<i64>().ok()),
"--nonce" => nonce = it.next(),
"--signature" => sig = it.next(),
"--nick" => nick = it.next(),
other => fail(&format!("unknown argument {other}")),
}
}
let (Some(node), Some(ts), Some(nonce), Some(sig)) = (node, ts, nonce, sig) else {
fail("node-enroll needs --node-id --ts --nonce --signature");
};
let url = std::env::var("M4A_PRODUCT_URL").ok().filter(|v| !v.is_empty()).unwrap_or_else(|| fail("M4A_PRODUCT_URL is required"));
let body = serde_json::json!({ "node_id": node, "nick": nick, "ts_ms": ts, "nonce": nonce, "signature": sig });
let client = reqwest::blocking::Client::builder().timeout(std::time::Duration::from_secs(30)).build().unwrap_or_else(|e| fail(&e.to_string()));
let resp = client
.post(format!("{}/product/v1/node/accounts", url.trim_end_matches('/')))
.json(&body)
.send()
.unwrap_or_else(|e| fail(&format!("product unreachable: {}", e.without_url())));
let status = resp.status().as_u16();
let v: serde_json::Value = resp.json().unwrap_or_default();
if status != 200 {
fail(&format!("node registration refused ({status}): {}", v.get("error").and_then(|e| e.as_str()).unwrap_or("no reason")));
}
let Some(invite) = v.get("invite").and_then(|i| i.as_str()) else { fail("product answer has no invite") };
let me = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("m4a-agent"));
let out = std::process::Command::new(me)
.arg("enroll")
.env(crate::PRODUCT_INVITE_ENV, invite)
.env_remove(crate::session_account::SESSION_CAP_ENV)
.output()
.unwrap_or_else(|e| fail(&format!("enroll: {}", e.kind())));
std::io::stderr().write_all(&out.stderr).ok();
std::io::stdout().write_all(&out.stdout).ok();
if !out.status.success() {
std::process::exit(out.status.code().unwrap_or(1));
}
}