feat(cli/org): org init — structure + wrap + configure_git_signing + signed bootstrap commit
This commit is contained in:
@@ -1,7 +1,96 @@
|
||||
//! `relicario org` subcommands for multi-user org vault management.
|
||||
|
||||
use anyhow::Result;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
pub fn run_init(_dir: &std::path::Path, _name: &str) -> Result<()> {
|
||||
todo!("org init")
|
||||
use anyhow::{Context, Result};
|
||||
use relicario_core::{
|
||||
generate_org_key, wrap_org_key,
|
||||
CollectionDef, MemberId, OrgCollections, OrgManifest, OrgMembers, OrgMeta, OrgRole, OrgMember,
|
||||
encrypt_org_manifest,
|
||||
};
|
||||
|
||||
use crate::org_session::atomic_write;
|
||||
|
||||
pub fn run_init(dir: &Path, name: &str) -> Result<()> {
|
||||
// Create directory structure
|
||||
fs::create_dir_all(dir.join("items")).context("create items/")?;
|
||||
fs::create_dir_all(dir.join("keys")).context("create keys/")?;
|
||||
|
||||
// Get caller's device info
|
||||
let device_pubkey = crate::device::current_device_pubkey()
|
||||
.context("read device key — run `relicario device add` first")?;
|
||||
|
||||
// Generate org master key
|
||||
let org_key = generate_org_key();
|
||||
|
||||
// Wrap org key to caller's device key
|
||||
let wrapped = wrap_org_key(&org_key, &device_pubkey)
|
||||
.context("wrap org key to device key")?;
|
||||
|
||||
// Create initial members.json with caller as owner
|
||||
let caller_id = MemberId::new();
|
||||
let now = relicario_core::now_unix();
|
||||
let member = OrgMember {
|
||||
member_id: caller_id.clone(),
|
||||
display_name: whoami(),
|
||||
role: OrgRole::Owner,
|
||||
ed25519_pubkey: device_pubkey,
|
||||
collections: vec![],
|
||||
added_at: now,
|
||||
added_by: caller_id.clone(),
|
||||
};
|
||||
let mut members = OrgMembers::new();
|
||||
members.members.push(member);
|
||||
|
||||
// Write wrapped key
|
||||
let key_path = dir.join("keys").join(format!("{}.enc", caller_id.as_str()));
|
||||
fs::write(&key_path, &wrapped).context("write caller key blob")?;
|
||||
|
||||
// Write org.json
|
||||
let meta = OrgMeta::new(name.to_string());
|
||||
let meta_json = serde_json::to_string_pretty(&meta)?;
|
||||
atomic_write(&dir.join("org.json"), meta_json.as_bytes())?;
|
||||
|
||||
// Write members.json
|
||||
let members_json = serde_json::to_string_pretty(&members)?;
|
||||
atomic_write(&dir.join("members.json"), members_json.as_bytes())?;
|
||||
|
||||
// Write collections.json (empty)
|
||||
let collections = OrgCollections::new();
|
||||
let coll_json = serde_json::to_string_pretty(&collections)?;
|
||||
atomic_write(&dir.join("collections.json"), coll_json.as_bytes())?;
|
||||
|
||||
// Write empty manifest.enc
|
||||
let manifest = OrgManifest::new();
|
||||
let manifest_bytes = encrypt_org_manifest(&manifest, &org_key)?;
|
||||
atomic_write(&dir.join("manifest.enc"), &manifest_bytes)?;
|
||||
|
||||
// git init, then configure THIS repo to sign commits with the active device
|
||||
// key. Org commits must be signed; the pre-receive hook verifies every one.
|
||||
crate::helpers::git_run(dir, &["init"], "git init")?;
|
||||
let device_name = crate::device::current_device()?
|
||||
.ok_or_else(|| anyhow::anyhow!("no active device — run `relicario device add` first"))?;
|
||||
crate::device::configure_git_signing(dir, &device_name)
|
||||
.context("configure org repo signing")?;
|
||||
|
||||
// Stage everything and make the signed bootstrap commit via org_git_run
|
||||
// (which does NOT disable signing, unlike helpers::git_run).
|
||||
crate::org_session::org_git_run(dir, &["add", "."], "git add")?;
|
||||
let commit_msg = format!(
|
||||
"init: org vault \"{name}\"\n\nRelicario-Actor: {} {}\nRelicario-Action: org-init",
|
||||
members.members[0].display_name,
|
||||
caller_id.as_str()
|
||||
);
|
||||
crate::org_session::org_git_run(dir, &["commit", "-m", &commit_msg], "git commit")?;
|
||||
|
||||
println!("Org vault initialized at {}", dir.display());
|
||||
println!("Your member ID: {}", caller_id.as_str());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn whoami() -> String {
|
||||
std::env::var("USER")
|
||||
.or_else(|_| std::env::var("USERNAME"))
|
||||
.unwrap_or_else(|_| "unknown".into())
|
||||
}
|
||||
|
||||
@@ -207,6 +207,12 @@ enum Commands {
|
||||
#[command(subcommand)]
|
||||
cmd: RecoveryQrCmd,
|
||||
},
|
||||
|
||||
/// Multi-user org vault operations.
|
||||
Org {
|
||||
#[command(subcommand)]
|
||||
action: OrgAction,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
@@ -422,6 +428,19 @@ pub(crate) enum RecoveryQrCmd {
|
||||
Unwrap,
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand)]
|
||||
pub(crate) enum OrgAction {
|
||||
/// Initialize a new org vault (creates dir structure, git repo, signed commit).
|
||||
Init {
|
||||
/// Directory to create the org vault in.
|
||||
#[arg(long)]
|
||||
dir: PathBuf,
|
||||
/// Human-readable display name for the org.
|
||||
#[arg(long)]
|
||||
name: String,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
match cli.command {
|
||||
@@ -456,6 +475,9 @@ fn main() -> Result<()> {
|
||||
Commands::Rate { passphrase } => commands::rate::cmd_rate(passphrase),
|
||||
Commands::Device { action } => commands::device::cmd_device(action),
|
||||
Commands::RecoveryQr { cmd } => commands::recovery_qr::cmd_recovery_qr(cmd),
|
||||
Commands::Org { action } => match action {
|
||||
OrgAction::Init { dir, name } => commands::org::run_init(&dir, &name),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user