Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,008 changes: 908 additions & 100 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ members = [
"crates/auths-infra-git",
"crates/auths-infra-http",
"crates/auths-storage",
"crates/auths-transparency",
"crates/auths-keri",
"crates/auths-jwt",
"crates/auths-mcp-server",
Expand Down Expand Up @@ -61,6 +62,7 @@ auths-jwt = { path = "crates/auths-jwt", version = "0.0.1-rc.7" }
auths-pairing-daemon = { path = "crates/auths-pairing-daemon", version = "0.0.1-rc.8" }
auths-pairing-protocol = { path = "crates/auths-pairing-protocol", version = "0.0.1-rc.7" }
auths-storage = { path = "crates/auths-storage", version = "0.0.1-rc.4" }
auths-transparency = { path = "crates/auths-transparency", version = "0.0.1-rc.8", default-features = false }
auths-utils = { path = "crates/auths-utils" }
insta = { version = "1", features = ["json"] }

Expand Down
1 change: 1 addition & 0 deletions crates/auths-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ auths-policy.workspace = true
auths-index.workspace = true
auths-crypto.workspace = true
auths-sdk.workspace = true
auths-transparency = { workspace = true, features = ["native"] }
auths-pairing-protocol.workspace = true
auths-telemetry = { workspace = true, features = ["sink-http"] }
auths-verifier = { workspace = true, features = ["native"] }
Expand Down
9 changes: 9 additions & 0 deletions crates/auths-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::path::PathBuf;
use clap::builder::styling::{AnsiColor, Effects, Styles};
use clap::{Parser, Subcommand};

use crate::commands::account::AccountCommand;
use crate::commands::agent::AgentCommand;
use crate::commands::approval::ApprovalCommand;
use crate::commands::artifact::ArtifactCommand;
Expand All @@ -21,6 +22,8 @@ use crate::commands::id::IdCommand;
use crate::commands::init::InitCommand;
use crate::commands::key::KeyCommand;
use crate::commands::learn::LearnCommand;
use crate::commands::log::LogCommand;
use crate::commands::namespace::NamespaceCommand;
use crate::commands::org::OrgCommand;
use crate::commands::policy::PolicyCommand;
use crate::commands::scim::ScimCommand;
Expand Down Expand Up @@ -119,6 +122,8 @@ pub enum RootCommand {
#[command(hide = true)]
Trust(TrustCommand),
#[command(hide = true)]
Namespace(NamespaceCommand),
#[command(hide = true)]
Org(OrgCommand),
#[command(hide = true)]
Audit(AuditCommand),
Expand All @@ -135,4 +140,8 @@ pub enum RootCommand {
Commit(CommitCmd),
#[command(hide = true)]
Debug(DebugCmd),
#[command(hide = true)]
Log(LogCommand),
#[command(hide = true)]
Account(AccountCommand),
}
119 changes: 119 additions & 0 deletions crates/auths-cli/src/commands/account.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
use anyhow::Result;
use clap::{Parser, Subcommand};
use serde::Deserialize;

use super::executable::ExecutableCommand;
use crate::config::CliConfig;

/// Manage your registry account and view usage.
#[derive(Parser, Debug, Clone)]
pub struct AccountCommand {
#[clap(subcommand)]
pub subcommand: AccountSubcommand,
}

#[derive(Subcommand, Debug, Clone)]
pub enum AccountSubcommand {
/// Show account status and rate limits
Status {
/// Registry URL to query
#[arg(long, default_value = "https://registry.auths.dev")]
registry_url: String,
},
/// Show API usage history
Usage {
/// Registry URL to query
#[arg(long, default_value = "https://registry.auths.dev")]
registry_url: String,
/// Number of days to show
#[arg(long, default_value = "7")]
days: u32,
},
}

#[derive(Debug, Deserialize)]
struct AccountStatusResponse {
did: String,
tier: String,
daily_limit: i32,
daily_used: i32,
expires_at: Option<String>,
}

#[derive(Debug, Deserialize)]
struct UsageEntry {
date: String,
request_count: i32,
}

fn handle_status(registry_url: &str) -> Result<()> {
let url = registry_url.trim_end_matches('/');

println!("Fetching account status...");

let client = reqwest::blocking::Client::new();
let resp = client
.get(format!("{url}/v1/account/status"))
.send()
.map_err(|e| anyhow::anyhow!("Failed to fetch account status: {e}"))?;

if !resp.status().is_success() {
return Err(anyhow::anyhow!("Registry returned {}", resp.status()));
}

let status: AccountStatusResponse = resp
.json()
.map_err(|e| anyhow::anyhow!("Failed to parse response: {e}"))?;

println!("\nAccount Status:");
println!(" DID: {}", status.did);
println!(" Tier: {}", status.tier);
println!(" Daily Limit: {}", status.daily_limit);
println!(" Daily Used: {}", status.daily_used);
if let Some(expires) = status.expires_at {
println!(" Expires: {expires}");
}

Ok(())
}

fn handle_usage(registry_url: &str, days: u32) -> Result<()> {
let url = registry_url.trim_end_matches('/');

println!("Fetching usage history ({days} days)...");

let client = reqwest::blocking::Client::new();
let resp = client
.get(format!("{url}/v1/account/usage?days={days}"))
.send()
.map_err(|e| anyhow::anyhow!("Failed to fetch usage: {e}"))?;

if !resp.status().is_success() {
return Err(anyhow::anyhow!("Registry returned {}", resp.status()));
}

let entries: Vec<UsageEntry> = resp
.json()
.map_err(|e| anyhow::anyhow!("Failed to parse response: {e}"))?;

if entries.is_empty() {
println!("\nNo usage data found.");
return Ok(());
}

println!("\nUsage History:");
for entry in &entries {
println!(" {} -- {} requests", entry.date, entry.request_count);
}

Ok(())
}

impl ExecutableCommand for AccountCommand {
fn execute(&self, _ctx: &CliConfig) -> Result<()> {
match &self.subcommand {
AccountSubcommand::Status { registry_url } => handle_status(registry_url),
AccountSubcommand::Usage { registry_url, days } => handle_usage(registry_url, *days),
}
}
}
62 changes: 61 additions & 1 deletion crates/auths-cli/src/commands/artifact/publish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ use std::time::Duration;
use anyhow::{Context, Result, bail};
use auths_infra_http::HttpRegistryClient;
use auths_sdk::workflows::artifact::{
ArtifactPublishConfig, ArtifactPublishError, publish_artifact,
ArtifactPublishConfig, ArtifactPublishError, ArtifactPublishResult, publish_artifact,
};
use auths_transparency::OfflineBundle;
use auths_verifier::core::ResourceId;
use serde::Serialize;

Expand Down Expand Up @@ -121,6 +122,9 @@ async fn handle_publish_async(
other => anyhow::anyhow!("{}", other),
})?;

// Cache checkpoint from bundle if present in the signature file
cache_checkpoint_from_sig(&sig_contents);

if is_json_mode() {
let json_resp = JsonResponse::success(
"artifact publish",
Expand Down Expand Up @@ -150,7 +154,63 @@ async fn handle_publish_async(
registry_url, pkg
);
}
display_rate_limit(&out, &body);
}

Ok(())
}

fn display_rate_limit(out: &Output, result: &ArtifactPublishResult) {
let Some(ref rl) = result.rate_limit else {
return;
};
println!();
if let Some(tier) = &rl.tier {
println!(" Tier: {}", out.info(tier));
}
if let (Some(remaining), Some(limit)) = (rl.remaining, rl.limit) {
println!(
" Quota: {}/{} requests remaining today",
out.bold(&remaining.to_string()),
limit
);
}
if let Some(reset) = rl.reset
&& let Some(dt) = chrono::DateTime::from_timestamp(reset, 0)
{
let human = dt.format("%Y-%m-%d %H:%M UTC");
println!(" Resets at: {human}");
}
}

/// Best-effort checkpoint caching after publish, using the bundle in the sig file.
#[allow(clippy::disallowed_methods)] // CLI is the presentation boundary
fn cache_checkpoint_from_sig(sig_contents: &str) {
let sig_value: serde_json::Value = match serde_json::from_str(sig_contents) {
Ok(v) => v,
Err(_) => return,
};

if sig_value.get("offline_bundle").is_none() {
return;
}

let bundle: OfflineBundle = match serde_json::from_value(sig_value["offline_bundle"].clone()) {
Ok(b) => b,
Err(_) => return,
};

let cache_path = match dirs::home_dir() {
Some(home) => home.join(".auths").join("log_checkpoint.json"),
None => return,
};

if let Err(e) = auths_sdk::workflows::transparency::try_cache_checkpoint(
&cache_path,
&bundle.signed_checkpoint,
None,
) && !is_json_mode()
{
eprintln!("Warning: checkpoint cache update failed: {e}");
}
}
Loading
Loading