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
15 changes: 13 additions & 2 deletions src/acme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@ use crate::cli::AcmeConfig;
use anyhow::{Context, Result};
use std::path::PathBuf;

/// Resolve the certificate cache directory.
/// Resolves the certificate cache directory.
///
/// Defaults to `~/.grob/certs/` if not specified.
///
/// # Errors
///
/// Returns an error if the home directory cannot be determined or
/// the cache directory cannot be created.
pub fn resolve_cache_dir(config: &AcmeConfig) -> Result<PathBuf> {
let dir = if config.cache_dir.is_empty() {
crate::grob_home()
Expand All @@ -26,10 +32,15 @@ pub fn resolve_cache_dir(config: &AcmeConfig) -> Result<PathBuf> {
Ok(dir)
}

/// Build the rustls-acme AcmeConfig and return an AxumAcceptor.
/// Builds the rustls-acme AcmeConfig and returns an AxumAcceptor.
///
/// Returns an `axum_server::accept::Accept`-compatible acceptor.
/// Spawns the ACME event loop in the background for certificate renewals.
///
/// # Errors
///
/// Returns an error if the certificate cache directory cannot be
/// resolved or created.
#[cfg(feature = "acme")]
pub fn build_acme_acceptor(config: &AcmeConfig) -> Result<rustls_acme::axum::AxumAcceptor> {
use futures::StreamExt;
Expand Down
22 changes: 19 additions & 3 deletions src/auth/jwt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,12 @@ pub struct JwtValidator {
}

impl JwtValidator {
/// Create a JwtValidator from config.
/// Creates a [`JwtValidator`] from config.
///
/// # Errors
///
/// Returns an error if the HMAC secret or JWKS URL in `config`
/// cannot be parsed into valid decoding keys.
pub fn from_config(config: &JwtConfig) -> Result<Self> {
let hmac_key = if !config.hmac_secret.is_empty() {
Some(DecodingKey::from_secret(config.hmac_secret.as_bytes()))
Expand Down Expand Up @@ -110,10 +115,16 @@ impl JwtValidator {
})
}

/// Validate a JWT token string and extract claims.
/// Validates a JWT token string and extracts claims.
///
/// Uses an in-memory cache keyed by SHA-256(token) to avoid repeated
/// cryptographic signature verification (5 min TTL).
///
/// # Errors
///
/// Returns [`AuthError::InvalidToken`] if the signature is invalid
/// or no configured key (HMAC / JWKS RSA / JWKS EC) can verify it.
/// Returns [`AuthError`] variants for expired or malformed tokens.
pub fn validate(&self, token: &str) -> Result<GrobClaims, AuthError> {
use sha2::{Digest, Sha256};

Expand Down Expand Up @@ -186,7 +197,12 @@ impl JwtValidator {
!ec.is_empty()
}

/// Refresh JWKS keys from the configured URL.
/// Refreshes JWKS keys from the configured URL.
///
/// # Errors
///
/// Returns an error if the HTTP request to the JWKS URL fails
/// or the response cannot be parsed as a JSON Web Key Set.
pub async fn refresh_jwks(&self) -> Result<()> {
let url = match &self.jwks_url {
Some(url) => url.clone(),
Expand Down
25 changes: 21 additions & 4 deletions src/auth/oauth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,11 @@ impl OAuthClient {
}
}

/// Generate authorization URL with PKCE
/// Generates the authorization URL with PKCE challenge.
///
/// # Errors
///
/// Returns an error if the configured `auth_url` is not a valid URL.
pub fn authorization_url(&self) -> Result<AuthorizationUrl> {
let pkce = PKCEVerifier::generate();

Expand Down Expand Up @@ -371,7 +375,13 @@ impl OAuthClient {
}
}

/// Refresh an access token
/// Refreshes an access token using the stored refresh token.
///
/// # Errors
///
/// Returns an error if no token exists for `provider_id`, the HTTP
/// request to the token endpoint fails, or the provider rejects
/// the refresh request.
pub async fn refresh_token(&self, provider_id: &str) -> Result<OAuthToken> {
let existing_token = self
.token_store
Expand Down Expand Up @@ -475,8 +485,15 @@ impl OAuthClient {
.context("OAuth token request failed")
}

/// Load Code Assist for Gemini and get project ID
/// This must be called after OAuth exchange for Gemini providers
/// Loads Code Assist for Gemini and returns the project ID.
///
/// Must be called after OAuth exchange for Gemini providers.
///
/// # Errors
///
/// Returns an error if the `loadCodeAssist` API call fails,
/// the provider returns a non-success HTTP status, or the response
/// does not contain a `cloudaicompanionProject` field.
pub async fn load_code_assist(&self, access_token: &str) -> Result<String> {
#[derive(Serialize)]
struct LoadCodeAssistRequest {
Expand Down
42 changes: 36 additions & 6 deletions src/auth/token_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,12 @@ pub struct TokenStore {
}

impl TokenStore {
/// Create a new token store backed by GrobStore.
/// Creates a new token store backed by [`GrobStore`](crate::storage::GrobStore).
///
/// # Errors
///
/// Returns an error if loading existing tokens from the
/// database fails.
pub fn with_store(store: std::sync::Arc<crate::storage::GrobStore>) -> Result<Self> {
let tokens = store.all_oauth_tokens();
Ok(Self {
Expand All @@ -122,7 +127,12 @@ impl TokenStore {
}
}

/// Create a new token store (legacy JSON mode).
/// Creates a new token store (legacy JSON mode).
///
/// # Errors
///
/// Returns an error if the token file exists but cannot be read
/// or parsed as JSON.
pub fn new(file_path: PathBuf) -> Result<Self> {
let tokens = if file_path.exists() {
let content = fs::read_to_string(&file_path).context("Failed to read token file")?;
Expand All @@ -138,21 +148,36 @@ impl TokenStore {
})
}

/// Get default token store path
/// Gets the default token store path.
///
/// # Errors
///
/// Returns an error if the home directory cannot be determined
/// or the config directory cannot be created.
pub fn default_path() -> Result<PathBuf> {
let home = crate::home_dir().context("Failed to get home directory (set GROB_HOME)")?;
let config_dir = home.join(".grob");
fs::create_dir_all(&config_dir).context("Failed to create config directory")?;
Ok(config_dir.join("oauth_tokens.json"))
}

/// Create a token store at the default location (legacy mode).
/// Creates a token store at the default location (legacy mode).
///
/// # Errors
///
/// Returns an error if the default path cannot be resolved or
/// the token file cannot be read.
pub fn at_default_path() -> Result<Self> {
let path = Self::default_path()?;
Self::new(path)
}

/// Save token for a provider
/// Saves a token for a provider.
///
/// # Errors
///
/// Returns an error if the database write or legacy JSON
/// persistence fails.
pub fn save(&self, token: OAuthToken) -> Result<()> {
let provider_id = token.provider_id.clone();

Expand All @@ -178,7 +203,12 @@ impl TokenStore {
tokens.get(provider_id).cloned()
}

/// Remove token for a provider
/// Removes a token for a provider.
///
/// # Errors
///
/// Returns an error if the database deletion or legacy JSON
/// persistence fails.
pub fn remove(&self, provider_id: &str) -> Result<()> {
if let Some(ref store) = self.store {
store.delete_oauth_token(provider_id)?;
Expand Down
29 changes: 24 additions & 5 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,12 @@ impl AppConfig {
!path.exists()
}

/// Get default config file path
/// Returns ~/.grob/config.toml (cross-platform)
/// Returns the default config file path (`~/.grob/config.toml`).
///
/// # Errors
///
/// Returns an error if the home directory cannot be determined
/// or the config directory cannot be created.
pub fn default_path() -> Result<PathBuf> {
let config_dir =
crate::grob_home().context("Failed to get home directory (set GROB_HOME)")?;
Expand All @@ -125,7 +129,12 @@ impl AppConfig {
Ok(config_dir.join("config.toml"))
}

/// Load configuration from a TOML file
/// Loads configuration from a TOML file.
///
/// # Errors
///
/// Returns an error if the file cannot be read, the TOML content
/// is malformed, or config validation fails.
pub fn from_file(path: &Path) -> Result<Self> {
// Check if file exists, if not create a default one
if !path.exists() {
Expand All @@ -138,7 +147,12 @@ impl AppConfig {
Self::from_content(&content, &format!("{}", path.display()))
}

/// Load configuration from a ConfigSource (file path or URL)
/// Loads configuration from a [`ConfigSource`] (file path or URL).
///
/// # Errors
///
/// Returns an error if the file/URL cannot be read, the HTTP
/// request fails, or the TOML content is invalid.
pub async fn from_source(source: &ConfigSource) -> Result<Self> {
match source {
ConfigSource::File(path) => Self::from_file(path),
Expand All @@ -156,7 +170,12 @@ impl AppConfig {
}
}

/// Parse configuration from TOML content string
/// Parses configuration from a TOML content string.
///
/// # Errors
///
/// Returns an error if the TOML cannot be deserialized, environment
/// variable resolution fails, or config validation fails.
pub fn from_content(content: &str, source_label: &str) -> Result<Self> {
let mut config: AppConfig = toml::from_str(content)
.with_context(|| format!("Failed to parse config from {}", source_label))?;
Expand Down
12 changes: 12 additions & 0 deletions src/cli/newtypes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ pub struct BudgetUsd(f64);

impl BudgetUsd {
/// Creates a new `BudgetUsd`, returning an error if negative.
///
/// # Errors
///
/// Returns a `String` if `value` is negative.
pub fn new(value: f64) -> Result<Self, String> {
if value < 0.0 {
Err(format!("budget_usd must be non-negative, got {}", value))
Expand Down Expand Up @@ -47,6 +51,10 @@ pub struct Port(u16);

impl Port {
/// Creates a new `Port`, returning an error if 0.
///
/// # Errors
///
/// Returns a `String` if `value` is zero.
pub fn new(value: u16) -> Result<Self, String> {
if value == 0 {
Err("port must be non-zero".to_string())
Expand Down Expand Up @@ -120,6 +128,10 @@ pub struct BodySizeLimit(usize);

impl BodySizeLimit {
/// Creates a new `BodySizeLimit`, returning an error if 0.
///
/// # Errors
///
/// Returns a `String` if `value` is zero.
pub fn new(value: usize) -> Result<Self, String> {
if value == 0 {
Err("max_body_size must be non-zero".to_string())
Expand Down
8 changes: 7 additions & 1 deletion src/cli/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ use std::collections::HashSet;
use super::{AppConfig, ModelStrategy};

impl AppConfig {
/// Validate configuration for common errors
/// Validates configuration for common errors.
///
/// # Errors
///
/// Returns an error if model mappings reference unknown providers,
/// enabled providers lack required auth credentials, or configured
/// regex patterns fail to compile.
pub fn validate(&self) -> Result<()> {
let provider_names: HashSet<&str> =
self.providers.iter().map(|p| p.name.as_str()).collect();
Expand Down
10 changes: 10 additions & 0 deletions src/commands/config_promote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ use std::io::Write;
///
/// Loads the preset, fetches the remote config for diffing, optionally
/// prompts for confirmation, then uploads and triggers a reload.
///
/// # Errors
///
/// Returns an error if the preset cannot be loaded, the remote
/// instance is unreachable, or the config push/reload fails.
pub async fn cmd_config_push(name: &str, target_url: &str, skip_confirm: bool) -> Result<()> {
let preset_toml = preset::preset_content(name)
.with_context(|| format!("Failed to load preset '{}'", name))?;
Expand Down Expand Up @@ -123,6 +128,11 @@ pub async fn cmd_config_push(name: &str, target_url: &str, skip_confirm: bool) -
///
/// Fetches the remote `/api/config` JSON, strips the `server` section
/// (host/port are not portable), and saves as a TOML preset file.
///
/// # Errors
///
/// Returns an error if the remote instance is unreachable, the
/// response cannot be parsed, or the preset file cannot be written.
pub async fn cmd_config_pull(from_url: &str, save_name: &str) -> Result<()> {
let client = reqwest::Client::new();
let timeout = std::time::Duration::from_secs(10);
Expand Down
2 changes: 1 addition & 1 deletion src/commands/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -730,7 +730,7 @@ fn providers_from_preset(name: &str) -> Vec<String> {
///
/// # Errors
///
/// Returns an error if config writing fails or the backup copy fails.
/// Returns an error if config writing, backup, or credential setup fails.
pub async fn run_setup_wizard(config_path: &Path, flags: &SetupFlags) -> Result<bool> {
println!();

Expand Down
7 changes: 6 additions & 1 deletion src/features/dlp/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,12 @@ struct RulesFile {
}

impl DlpConfig {
/// Load and merge rules from an external file, if `rules_file` is set.
/// Loads and merges rules from an external file, if `rules_file` is set.
///
/// # Errors
///
/// Returns an error if the rules file cannot be read or parsed
/// as TOML.
pub fn load_external_rules(&mut self) -> Result<()> {
if self.rules_file.is_empty() {
return Ok(());
Expand Down
7 changes: 6 additions & 1 deletion src/features/dlp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -627,7 +627,12 @@ impl DlpEngine {
(result, reports)
}

/// Check response text for URL exfiltration block. Returns error if blocked.
/// Checks response text for URL exfiltration attempts.
///
/// # Errors
///
/// Returns [`DlpBlockError::UrlExfilBlocked`] if the text contains
/// a URL matching the exfiltration pattern.
pub fn check_response_url_exfil(&self, text: &str) -> Result<(), DlpBlockError> {
if let Some(ref exfil) = self.url_exfil_scanner {
if let Some(dets) = exfil.is_blocked(text) {
Expand Down
7 changes: 6 additions & 1 deletion src/features/dlp/signed_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ struct InjectionOverrides {
custom_patterns: Vec<String>,
}

/// Load a public key from a PEM file or raw SEC1 bytes.
/// Loads a P-256 public key from a PEM file or raw SEC1 bytes.
///
/// # Errors
///
/// Returns an error if the file cannot be read, the PEM base64 is
/// invalid, or the key material is not a valid P-256 public key.
pub fn load_public_key(path: &str) -> Result<VerifyingKey> {
let data =
std::fs::read(path).with_context(|| format!("Failed to read public key: {}", path))?;
Expand Down
Loading
Loading