feat(cli/org): org add parity for Card/Key/Totp via shared builders

This commit is contained in:
adlee-was-taken
2026-06-20 18:31:29 -04:00
parent 07862b8d44
commit 82feb49ab4
2 changed files with 110 additions and 40 deletions

View File

@@ -6,12 +6,13 @@ use std::path::Path;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use relicario_core::{ use relicario_core::{
generate_org_key, wrap_org_key, generate_org_key, wrap_org_key,
CollectionDef, Item, ItemCore, MemberId, OrgCollections, OrgManifest, OrgMembers, OrgMeta, CollectionDef, Item, MemberId, OrgCollections, OrgManifest, OrgMembers, OrgMeta,
OrgRole, OrgMember, OrgRole, OrgMember,
encrypt_org_manifest, encrypt_org_manifest,
}; };
use crate::org_session::atomic_write; use crate::org_session::atomic_write;
use crate::commands::item_build as ib;
pub fn run_init(dir: &Path, name: &str) -> Result<()> { pub fn run_init(dir: &Path, name: &str) -> Result<()> {
// Create directory structure // Create directory structure
@@ -745,17 +746,20 @@ pub fn run_audit(
Ok(()) Ok(())
} }
/// Item kinds `org add` supports without interactive prompts. /// Item kinds `org add` supports. Secrets resolve via `--*-stdin` flags or an
/// interactive prompt inside the shared `item_build` builders.
pub enum OrgAddKind { pub enum OrgAddKind {
Login { Login {
title: String, title: String,
username: Option<String>, username: Option<String>,
url: Option<String>, url: Option<String>,
password: Option<String>, password: Option<String>,
password_stdin: bool,
}, },
SecureNote { SecureNote {
title: String, title: String,
body: String, body: Option<String>,
body_stdin: bool,
}, },
Identity { Identity {
title: String, title: String,
@@ -763,43 +767,56 @@ pub enum OrgAddKind {
email: Option<String>, email: Option<String>,
phone: Option<String>, phone: Option<String>,
}, },
Card {
title: String,
holder: Option<String>,
expiry: Option<String>,
kind: String,
number_stdin: bool,
cvv_stdin: bool,
pin_stdin: bool,
},
Key {
title: String,
label: Option<String>,
algorithm: Option<String>,
public_key: Option<String>,
material_stdin: bool,
},
Totp {
title: String,
issuer: Option<String>,
label: Option<String>,
secret: Option<String>,
secret_stdin: bool,
period: u32,
digits: u8,
algorithm: String,
},
// Document is added later by Dev-C.
} }
fn build_org_item(kind: OrgAddKind, tags: Vec<String>) -> Result<Item> { fn build_org_item(kind: OrgAddKind) -> Result<Item> {
use relicario_core::item_types::{IdentityCore, LoginCore, SecureNoteCore}; match kind {
use zeroize::Zeroizing; OrgAddKind::Login { title, username, url, password, password_stdin } => {
ib::build_login(title, username, url, password, password_stdin, false, None)
let mut item = match kind {
OrgAddKind::Login { title, username, url, password } => {
let parsed_url = match url {
Some(s) => Some(url::Url::parse(&s).with_context(|| format!("invalid URL: {s}"))?),
None => None,
};
let password = password.map(Zeroizing::new);
Item::new(title, ItemCore::Login(LoginCore {
username,
password,
url: parsed_url,
totp: None,
}))
} }
OrgAddKind::SecureNote { title, body } => { OrgAddKind::SecureNote { title, body, body_stdin } => {
Item::new(title, ItemCore::SecureNote(SecureNoteCore { ib::build_secure_note(title, body, body_stdin)
body: Zeroizing::new(body),
}))
} }
OrgAddKind::Identity { title, full_name, email, phone } => { OrgAddKind::Identity { title, full_name, email, phone } => {
Item::new(title, ItemCore::Identity(IdentityCore { ib::build_identity(title, full_name, email, phone, None)
full_name, }
address: None, OrgAddKind::Card { title, holder, expiry, kind, number_stdin, cvv_stdin, pin_stdin } => {
phone, ib::build_card(title, holder, expiry, &kind, number_stdin, cvv_stdin, pin_stdin)
email, }
date_of_birth: None, OrgAddKind::Key { title, label, algorithm, public_key, material_stdin } => {
})) ib::build_key(title, label, algorithm, public_key, material_stdin)
}
OrgAddKind::Totp { title, issuer, label, secret, secret_stdin, period, digits, algorithm } => {
ib::build_totp(title, issuer, label, secret, secret_stdin, period, digits, &algorithm)
}
} }
};
item.tags = tags;
Ok(item)
} }
pub fn run_add(dir: &Path, collection: &str, kind: OrgAddKind, tags: Vec<String>) -> Result<()> { pub fn run_add(dir: &Path, collection: &str, kind: OrgAddKind, tags: Vec<String>) -> Result<()> {
@@ -816,7 +833,8 @@ pub fn run_add(dir: &Path, collection: &str, kind: OrgAddKind, tags: Vec<String>
// …and the caller must hold a grant for it. // …and the caller must hold a grant for it.
UnlockedOrgVault::ensure_grant(&caller, collection)?; UnlockedOrgVault::ensure_grant(&caller, collection)?;
let item = build_org_item(kind, tags)?; let mut item = build_org_item(kind)?;
item.tags = tags;
let item_rel = vault.save_item(collection, &item)?; let item_rel = vault.save_item(collection, &item)?;
// Upsert the manifest entry (collection slug stored plaintext inside the // Upsert the manifest entry (collection slug stored plaintext inside the

View File

@@ -566,13 +566,15 @@ pub(crate) enum OrgAddKind {
#[arg(long)] url: Option<String>, #[arg(long)] url: Option<String>,
#[arg(long)] password: Option<String>, #[arg(long)] password: Option<String>,
#[arg(long, value_delimiter = ',')] tags: Vec<String>, #[arg(long, value_delimiter = ',')] tags: Vec<String>,
#[arg(long)] password_stdin: bool,
}, },
/// A secure note. /// A secure note.
SecureNote { SecureNote {
#[arg(long)] collection: String, #[arg(long)] collection: String,
#[arg(long)] title: String, #[arg(long)] title: String,
#[arg(long)] body: String, #[arg(long)] body: Option<String>,
#[arg(long, value_delimiter = ',')] tags: Vec<String>, #[arg(long, value_delimiter = ',')] tags: Vec<String>,
#[arg(long)] body_stdin: bool,
}, },
/// An identity record. /// An identity record.
Identity { Identity {
@@ -583,6 +585,41 @@ pub(crate) enum OrgAddKind {
#[arg(long)] phone: Option<String>, #[arg(long)] phone: Option<String>,
#[arg(long, value_delimiter = ',')] tags: Vec<String>, #[arg(long, value_delimiter = ',')] tags: Vec<String>,
}, },
/// A payment card (number / cvv / pin entered via --*-stdin or prompt).
Card {
#[arg(long)] collection: String,
#[arg(long)] title: String,
#[arg(long)] holder: Option<String>,
#[arg(long)] expiry: Option<String>,
#[arg(long, default_value = "credit")] kind: String,
#[arg(long, value_delimiter = ',')] tags: Vec<String>,
#[arg(long)] number_stdin: bool,
#[arg(long)] cvv_stdin: bool,
#[arg(long)] pin_stdin: bool,
},
/// A key / credential blob (material entered via --material-stdin or prompt).
Key {
#[arg(long)] collection: String,
#[arg(long)] title: String,
#[arg(long)] label: Option<String>,
#[arg(long)] algorithm: Option<String>,
#[arg(long)] public_key: Option<String>,
#[arg(long, value_delimiter = ',')] tags: Vec<String>,
#[arg(long)] material_stdin: bool,
},
/// A TOTP authenticator (base32 secret via --secret or --secret-stdin).
Totp {
#[arg(long)] collection: String,
#[arg(long)] title: String,
#[arg(long)] issuer: Option<String>,
#[arg(long)] label: Option<String>,
#[arg(long)] secret: Option<String>,
#[arg(long, default_value_t = 30)] period: u32,
#[arg(long, default_value_t = 6)] digits: u8,
#[arg(long, default_value = "sha1")] algorithm: String,
#[arg(long, value_delimiter = ',')] tags: Vec<String>,
#[arg(long)] secret_stdin: bool,
},
} }
fn main() -> Result<()> { fn main() -> Result<()> {
@@ -676,14 +713,14 @@ fn main() -> Result<()> {
OrgCommands::Add { kind } => { OrgCommands::Add { kind } => {
let d = crate::org_session::org_dir(dir_path)?; let d = crate::org_session::org_dir(dir_path)?;
let (collection, add_kind, tags) = match kind { let (collection, add_kind, tags) = match kind {
OrgAddKind::Login { collection, title, username, url, password, tags } => ( OrgAddKind::Login { collection, title, username, url, password, tags, password_stdin } => (
collection, collection,
commands::org::OrgAddKind::Login { title, username, url, password }, commands::org::OrgAddKind::Login { title, username, url, password, password_stdin },
tags, tags,
), ),
OrgAddKind::SecureNote { collection, title, body, tags } => ( OrgAddKind::SecureNote { collection, title, body, tags, body_stdin } => (
collection, collection,
commands::org::OrgAddKind::SecureNote { title, body }, commands::org::OrgAddKind::SecureNote { title, body, body_stdin },
tags, tags,
), ),
OrgAddKind::Identity { collection, title, full_name, email, phone, tags } => ( OrgAddKind::Identity { collection, title, full_name, email, phone, tags } => (
@@ -691,6 +728,21 @@ fn main() -> Result<()> {
commands::org::OrgAddKind::Identity { title, full_name, email, phone }, commands::org::OrgAddKind::Identity { title, full_name, email, phone },
tags, tags,
), ),
OrgAddKind::Card { collection, title, holder, expiry, kind, tags, number_stdin, cvv_stdin, pin_stdin } => (
collection,
commands::org::OrgAddKind::Card { title, holder, expiry, kind, number_stdin, cvv_stdin, pin_stdin },
tags,
),
OrgAddKind::Key { collection, title, label, algorithm, public_key, tags, material_stdin } => (
collection,
commands::org::OrgAddKind::Key { title, label, algorithm, public_key, material_stdin },
tags,
),
OrgAddKind::Totp { collection, title, issuer, label, secret, period, digits, algorithm, tags, secret_stdin } => (
collection,
commands::org::OrgAddKind::Totp { title, issuer, label, secret, secret_stdin, period, digits, algorithm },
tags,
),
}; };
commands::org::run_add(&d, &collection, add_kind, tags)?; commands::org::run_add(&d, &collection, add_kind, tags)?;
} }