Skip to content

Commit 06b396e

Browse files
authored
Merge pull request #90 from auths-dev/dev-publicRegistry
feat: add support for C2SP tlog-tiles for public registry
2 parents 81427f3 + fdc40d4 commit 06b396e

53 files changed

Lines changed: 8216 additions & 155 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 908 additions & 100 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ members = [
1414
"crates/auths-infra-git",
1515
"crates/auths-infra-http",
1616
"crates/auths-storage",
17+
"crates/auths-transparency",
1718
"crates/auths-keri",
1819
"crates/auths-jwt",
1920
"crates/auths-mcp-server",
@@ -61,6 +62,7 @@ auths-jwt = { path = "crates/auths-jwt", version = "0.0.1-rc.7" }
6162
auths-pairing-daemon = { path = "crates/auths-pairing-daemon", version = "0.0.1-rc.8" }
6263
auths-pairing-protocol = { path = "crates/auths-pairing-protocol", version = "0.0.1-rc.7" }
6364
auths-storage = { path = "crates/auths-storage", version = "0.0.1-rc.4" }
65+
auths-transparency = { path = "crates/auths-transparency", version = "0.0.1-rc.8", default-features = false }
6466
auths-utils = { path = "crates/auths-utils" }
6567
insta = { version = "1", features = ["json"] }
6668

crates/auths-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ auths-policy.workspace = true
3838
auths-index.workspace = true
3939
auths-crypto.workspace = true
4040
auths-sdk.workspace = true
41+
auths-transparency = { workspace = true, features = ["native"] }
4142
auths-pairing-protocol.workspace = true
4243
auths-telemetry = { workspace = true, features = ["sink-http"] }
4344
auths-verifier = { workspace = true, features = ["native"] }

crates/auths-cli/src/cli.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::path::PathBuf;
33
use clap::builder::styling::{AnsiColor, Effects, Styles};
44
use clap::{Parser, Subcommand};
55

6+
use crate::commands::account::AccountCommand;
67
use crate::commands::agent::AgentCommand;
78
use crate::commands::approval::ApprovalCommand;
89
use crate::commands::artifact::ArtifactCommand;
@@ -21,6 +22,8 @@ use crate::commands::id::IdCommand;
2122
use crate::commands::init::InitCommand;
2223
use crate::commands::key::KeyCommand;
2324
use crate::commands::learn::LearnCommand;
25+
use crate::commands::log::LogCommand;
26+
use crate::commands::namespace::NamespaceCommand;
2427
use crate::commands::org::OrgCommand;
2528
use crate::commands::policy::PolicyCommand;
2629
use crate::commands::scim::ScimCommand;
@@ -119,6 +122,8 @@ pub enum RootCommand {
119122
#[command(hide = true)]
120123
Trust(TrustCommand),
121124
#[command(hide = true)]
125+
Namespace(NamespaceCommand),
126+
#[command(hide = true)]
122127
Org(OrgCommand),
123128
#[command(hide = true)]
124129
Audit(AuditCommand),
@@ -135,4 +140,8 @@ pub enum RootCommand {
135140
Commit(CommitCmd),
136141
#[command(hide = true)]
137142
Debug(DebugCmd),
143+
#[command(hide = true)]
144+
Log(LogCommand),
145+
#[command(hide = true)]
146+
Account(AccountCommand),
138147
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
use anyhow::Result;
2+
use clap::{Parser, Subcommand};
3+
use serde::Deserialize;
4+
5+
use super::executable::ExecutableCommand;
6+
use crate::config::CliConfig;
7+
8+
/// Manage your registry account and view usage.
9+
#[derive(Parser, Debug, Clone)]
10+
pub struct AccountCommand {
11+
#[clap(subcommand)]
12+
pub subcommand: AccountSubcommand,
13+
}
14+
15+
#[derive(Subcommand, Debug, Clone)]
16+
pub enum AccountSubcommand {
17+
/// Show account status and rate limits
18+
Status {
19+
/// Registry URL to query
20+
#[arg(long, default_value = "https://registry.auths.dev")]
21+
registry_url: String,
22+
},
23+
/// Show API usage history
24+
Usage {
25+
/// Registry URL to query
26+
#[arg(long, default_value = "https://registry.auths.dev")]
27+
registry_url: String,
28+
/// Number of days to show
29+
#[arg(long, default_value = "7")]
30+
days: u32,
31+
},
32+
}
33+
34+
#[derive(Debug, Deserialize)]
35+
struct AccountStatusResponse {
36+
did: String,
37+
tier: String,
38+
daily_limit: i32,
39+
daily_used: i32,
40+
expires_at: Option<String>,
41+
}
42+
43+
#[derive(Debug, Deserialize)]
44+
struct UsageEntry {
45+
date: String,
46+
request_count: i32,
47+
}
48+
49+
fn handle_status(registry_url: &str) -> Result<()> {
50+
let url = registry_url.trim_end_matches('/');
51+
52+
println!("Fetching account status...");
53+
54+
let client = reqwest::blocking::Client::new();
55+
let resp = client
56+
.get(format!("{url}/v1/account/status"))
57+
.send()
58+
.map_err(|e| anyhow::anyhow!("Failed to fetch account status: {e}"))?;
59+
60+
if !resp.status().is_success() {
61+
return Err(anyhow::anyhow!("Registry returned {}", resp.status()));
62+
}
63+
64+
let status: AccountStatusResponse = resp
65+
.json()
66+
.map_err(|e| anyhow::anyhow!("Failed to parse response: {e}"))?;
67+
68+
println!("\nAccount Status:");
69+
println!(" DID: {}", status.did);
70+
println!(" Tier: {}", status.tier);
71+
println!(" Daily Limit: {}", status.daily_limit);
72+
println!(" Daily Used: {}", status.daily_used);
73+
if let Some(expires) = status.expires_at {
74+
println!(" Expires: {expires}");
75+
}
76+
77+
Ok(())
78+
}
79+
80+
fn handle_usage(registry_url: &str, days: u32) -> Result<()> {
81+
let url = registry_url.trim_end_matches('/');
82+
83+
println!("Fetching usage history ({days} days)...");
84+
85+
let client = reqwest::blocking::Client::new();
86+
let resp = client
87+
.get(format!("{url}/v1/account/usage?days={days}"))
88+
.send()
89+
.map_err(|e| anyhow::anyhow!("Failed to fetch usage: {e}"))?;
90+
91+
if !resp.status().is_success() {
92+
return Err(anyhow::anyhow!("Registry returned {}", resp.status()));
93+
}
94+
95+
let entries: Vec<UsageEntry> = resp
96+
.json()
97+
.map_err(|e| anyhow::anyhow!("Failed to parse response: {e}"))?;
98+
99+
if entries.is_empty() {
100+
println!("\nNo usage data found.");
101+
return Ok(());
102+
}
103+
104+
println!("\nUsage History:");
105+
for entry in &entries {
106+
println!(" {} -- {} requests", entry.date, entry.request_count);
107+
}
108+
109+
Ok(())
110+
}
111+
112+
impl ExecutableCommand for AccountCommand {
113+
fn execute(&self, _ctx: &CliConfig) -> Result<()> {
114+
match &self.subcommand {
115+
AccountSubcommand::Status { registry_url } => handle_status(registry_url),
116+
AccountSubcommand::Usage { registry_url, days } => handle_usage(registry_url, *days),
117+
}
118+
}
119+
}

crates/auths-cli/src/commands/artifact/publish.rs

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ use std::time::Duration;
44
use anyhow::{Context, Result, bail};
55
use auths_infra_http::HttpRegistryClient;
66
use auths_sdk::workflows::artifact::{
7-
ArtifactPublishConfig, ArtifactPublishError, publish_artifact,
7+
ArtifactPublishConfig, ArtifactPublishError, ArtifactPublishResult, publish_artifact,
88
};
9+
use auths_transparency::OfflineBundle;
910
use auths_verifier::core::ResourceId;
1011
use serde::Serialize;
1112

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

125+
// Cache checkpoint from bundle if present in the signature file
126+
cache_checkpoint_from_sig(&sig_contents);
127+
124128
if is_json_mode() {
125129
let json_resp = JsonResponse::success(
126130
"artifact publish",
@@ -150,7 +154,63 @@ async fn handle_publish_async(
150154
registry_url, pkg
151155
);
152156
}
157+
display_rate_limit(&out, &body);
153158
}
154159

155160
Ok(())
156161
}
162+
163+
fn display_rate_limit(out: &Output, result: &ArtifactPublishResult) {
164+
let Some(ref rl) = result.rate_limit else {
165+
return;
166+
};
167+
println!();
168+
if let Some(tier) = &rl.tier {
169+
println!(" Tier: {}", out.info(tier));
170+
}
171+
if let (Some(remaining), Some(limit)) = (rl.remaining, rl.limit) {
172+
println!(
173+
" Quota: {}/{} requests remaining today",
174+
out.bold(&remaining.to_string()),
175+
limit
176+
);
177+
}
178+
if let Some(reset) = rl.reset
179+
&& let Some(dt) = chrono::DateTime::from_timestamp(reset, 0)
180+
{
181+
let human = dt.format("%Y-%m-%d %H:%M UTC");
182+
println!(" Resets at: {human}");
183+
}
184+
}
185+
186+
/// Best-effort checkpoint caching after publish, using the bundle in the sig file.
187+
#[allow(clippy::disallowed_methods)] // CLI is the presentation boundary
188+
fn cache_checkpoint_from_sig(sig_contents: &str) {
189+
let sig_value: serde_json::Value = match serde_json::from_str(sig_contents) {
190+
Ok(v) => v,
191+
Err(_) => return,
192+
};
193+
194+
if sig_value.get("offline_bundle").is_none() {
195+
return;
196+
}
197+
198+
let bundle: OfflineBundle = match serde_json::from_value(sig_value["offline_bundle"].clone()) {
199+
Ok(b) => b,
200+
Err(_) => return,
201+
};
202+
203+
let cache_path = match dirs::home_dir() {
204+
Some(home) => home.join(".auths").join("log_checkpoint.json"),
205+
None => return,
206+
};
207+
208+
if let Err(e) = auths_sdk::workflows::transparency::try_cache_checkpoint(
209+
&cache_path,
210+
&bundle.signed_checkpoint,
211+
None,
212+
) && !is_json_mode()
213+
{
214+
eprintln!("Warning: checkpoint cache update failed: {e}");
215+
}
216+
}

0 commit comments

Comments
 (0)