From e77ed11290df945cc212b39e5d32d9c4d6fb13ef Mon Sep 17 00:00:00 2001 From: Nathael Bonnal <64804778+NathaelB@users.noreply.github.com> Date: Sat, 13 Jun 2026 03:53:06 +0200 Subject: [PATCH 1/3] release: prepare 0.2.1 (#18) * fix: refresh expired tokens, confirm destructive deletes, add logout * feat: prepare 0.2.1 --- CLAUDE.md | 46 --------- Cargo.lock | 8 +- Cargo.toml | 2 +- libs/ferriskey-cli-client/src/lib.rs | 34 +++++++ libs/ferriskey-cli-commands/src/client.rs | 4 + libs/ferriskey-cli-commands/src/lib.rs | 4 +- libs/ferriskey-cli-commands/src/realm.rs | 13 ++- libs/ferriskey-cli-commands/src/user.rs | 4 + libs/ferriskey-cli-core/src/auth.rs | 12 +++ libs/ferriskey-cli-core/src/client.rs | 7 ++ libs/ferriskey-cli-core/src/confirm.rs | 43 +++++++++ libs/ferriskey-cli-core/src/credentials.rs | 13 +++ libs/ferriskey-cli-core/src/lib.rs | 2 + libs/ferriskey-cli-core/src/realm.rs | 35 ++++--- libs/ferriskey-cli-core/src/session.rs | 103 +++++++++++++++++++-- libs/ferriskey-cli-core/src/user.rs | 7 ++ 16 files changed, 264 insertions(+), 73 deletions(-) delete mode 100644 CLAUDE.md create mode 100644 libs/ferriskey-cli-core/src/confirm.rs diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index fa1ce31..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,46 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Commands - -```bash -cargo build --release # Build all workspace crates -cargo test --workspace # Run all tests -cargo clippy --workspace --all-targets --all-features -- -D warnings # Lint (warnings are errors) -cargo test -p ferriskey-cli-core context::tests # Run a specific test module -``` - -## Architecture - -This is a Cargo workspace with 4 crates: - -- **`ferris-ctl`** (root) — Binary entry point. Parses CLI with `Cli::parse()`, passes to `ferriskey_cli_core::run()`. -- **`libs/ferriskey-cli-commands`** — Clap derive structs only. Defines `Cli`, `Commands` enum, and per-command `*Command`/`*Args` structs. No logic. -- **`libs/ferriskey-cli-core`** — Command dispatch and execution. `run()` matches on `Commands` variants and delegates to module handlers (`context.rs`, `client.rs`). Owns config management. -- **`libs/ferriskey-cli-client`** — `reqwest`-based HTTP client. `FerriskeyClient::new(base_url, prefix, token)` handles auth (Bearer token), request serialization, and response parsing. - -### Data flow - -``` -CLI args → Cli::parse() → ferriskey_cli_core::run() - → match Commands → handler (context/client) - → FerriskeyClient → REST API - → FileContextRepository → TOML config file -``` - -### Config storage - -Contexts (URL, client_id, client_secret, optional realm) are stored as TOML at `$XDG_CONFIG_HOME/ferriskey/config.toml`. `FileContextRepository` does atomic writes (temp file + rename). `ContextStore` holds the map and tracks the active context. - -### Output formatting - -Root-level `--output` / `-o` flag accepts `table` (default), `json`, or `yaml`. Each handler has format-specific render functions. - -### Unimplemented stubs - -Realm and User commands are defined in `ferriskey-cli-commands` but return `UnimplementedCommand` errors in `ferriskey-cli-core`. Client `get`/`delete` subcommands are also stubs. - -### OAuth2 token exchange - -`FerriskeyClient::exchange_client_credentials()` performs the client credentials flow before API calls that require authentication. diff --git a/Cargo.lock b/Cargo.lock index cdc6c02..ca4591c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -258,7 +258,7 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "ferris-ctl" -version = "0.2.0" +version = "0.2.1" dependencies = [ "clap", "ferriskey-cli-commands", @@ -267,7 +267,7 @@ dependencies = [ [[package]] name = "ferriskey-cli-client" -version = "0.2.0" +version = "0.2.1" dependencies = [ "reqwest", "serde", @@ -277,14 +277,14 @@ dependencies = [ [[package]] name = "ferriskey-cli-commands" -version = "0.2.0" +version = "0.2.1" dependencies = [ "clap", ] [[package]] name = "ferriskey-cli-core" -version = "0.2.0" +version = "0.2.1" dependencies = [ "base64", "ctrlc", diff --git a/Cargo.toml b/Cargo.toml index d0709cb..7dbfb36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ members = ["libs/ferriskey-cli-client", "libs/ferriskey-cli-commands", "libs/fer resolver = "2" [workspace.package] -version = "0.2.0" +version = "0.2.1" authors = ["nathaelb "] edition = "2024" license = "Apache-2.0" diff --git a/libs/ferriskey-cli-client/src/lib.rs b/libs/ferriskey-cli-client/src/lib.rs index 299b1b0..77e8e7a 100644 --- a/libs/ferriskey-cli-client/src/lib.rs +++ b/libs/ferriskey-cli-client/src/lib.rs @@ -645,6 +645,40 @@ impl FerriskeyClient { })) } + /// Exchange a refresh token for a fresh access token (OAuth2 `refresh_token` + /// grant). `client_secret` is sent only for confidential clients. + pub fn exchange_refresh_token( + &self, + realm: &str, + client_id: &str, + refresh_token: &str, + client_secret: Option<&str>, + ) -> Result { + let mut form: Vec<(&str, &str)> = vec![ + ("grant_type", "refresh_token"), + ("refresh_token", refresh_token), + ("client_id", client_id), + ]; + if let Some(secret) = client_secret { + form.push(("client_secret", secret)); + } + + let response = self + .http + .post(self.endpoint(&format!("realms/{realm}/protocol/openid-connect/token"))) + .header("Content-Type", "application/x-www-form-urlencoded") + .form(&form) + .send()?; + + if !response.status().is_success() { + let status = response.status(); + let body = response.text().unwrap_or_default(); + return Err(FerriskeyClientError::Api { status, body }); + } + + Ok(response.json::()?) + } + pub fn exchange_client_credentials( &self, realm: &str, diff --git a/libs/ferriskey-cli-commands/src/client.rs b/libs/ferriskey-cli-commands/src/client.rs index d39c94c..793a778 100644 --- a/libs/ferriskey-cli-commands/src/client.rs +++ b/libs/ferriskey-cli-commands/src/client.rs @@ -91,4 +91,8 @@ pub struct ClientDeleteArgs { /// Realm name. Defaults to the selected context realm. #[arg(long)] pub realm: Option, + + /// Skip the confirmation prompt (required in non-interactive shells). + #[arg(long, short = 'f', default_value_t = false)] + pub force: bool, } diff --git a/libs/ferriskey-cli-commands/src/lib.rs b/libs/ferriskey-cli-commands/src/lib.rs index e61c28e..d11f72b 100644 --- a/libs/ferriskey-cli-commands/src/lib.rs +++ b/libs/ferriskey-cli-commands/src/lib.rs @@ -14,7 +14,7 @@ pub use self::context::{ }; pub use self::login::LoginCommand; pub use self::realm::{ - ImportSource, RealmCommand, RealmImportArgs, RealmNameArgs, RealmSubcommand, + ImportSource, RealmCommand, RealmDeleteArgs, RealmImportArgs, RealmNameArgs, RealmSubcommand, }; pub use self::source::{ SourceAddArgs, SourceCommand, SourceKind, SourceRemoveArgs, SourceSubcommand, @@ -72,4 +72,6 @@ pub enum Commands { Source(source::SourceCommand), /// Sign in via the OAuth 2.0 Device Authorization Grant. Login(login::LoginCommand), + /// Remove the stored login session (deletes the credentials file). + Logout, } diff --git a/libs/ferriskey-cli-commands/src/realm.rs b/libs/ferriskey-cli-commands/src/realm.rs index df77296..636f527 100644 --- a/libs/ferriskey-cli-commands/src/realm.rs +++ b/libs/ferriskey-cli-commands/src/realm.rs @@ -20,7 +20,7 @@ pub enum RealmSubcommand { /// Create a realm. Create(RealmNameArgs), /// Delete a realm. - Delete(RealmNameArgs), + Delete(RealmDeleteArgs), /// Import a realm (settings, clients, roles, users) from an external source. Import(RealmImportArgs), } @@ -32,6 +32,17 @@ pub struct RealmNameArgs { pub name: String, } +/// Arguments for `realm delete`. +#[derive(Debug, Args)] +pub struct RealmDeleteArgs { + /// Realm name. + pub name: String, + + /// Skip the confirmation prompt (required in non-interactive shells). + #[arg(long, short = 'f', default_value_t = false)] + pub force: bool, +} + /// Source to import a realm from. #[derive(Clone, Debug, ValueEnum)] pub enum ImportSource { diff --git a/libs/ferriskey-cli-commands/src/user.rs b/libs/ferriskey-cli-commands/src/user.rs index dd4c67b..9b2c2ef 100644 --- a/libs/ferriskey-cli-commands/src/user.rs +++ b/libs/ferriskey-cli-commands/src/user.rs @@ -73,4 +73,8 @@ pub struct UserDeleteArgs { /// Realm name. Defaults to the selected context realm. #[arg(long)] pub realm: Option, + + /// Skip the confirmation prompt (required in non-interactive shells). + #[arg(long, short = 'f', default_value_t = false)] + pub force: bool, } diff --git a/libs/ferriskey-cli-core/src/auth.rs b/libs/ferriskey-cli-core/src/auth.rs index f168c49..b35ca7b 100644 --- a/libs/ferriskey-cli-core/src/auth.rs +++ b/libs/ferriskey-cli-core/src/auth.rs @@ -135,6 +135,18 @@ pub fn run( Ok(()) } +/// Remove the stored login session. Dispatched from `core::run` for the +/// `logout` command. +pub fn logout() -> Result<()> { + let removed = CredentialsRepository::new()?.delete()?; + if removed { + println!("Logged out. Stored credentials removed."); + } else { + println!("No active session; nothing to remove."); + } + Ok(()) +} + #[derive(Debug, Clone)] struct LoginTarget { url: String, diff --git a/libs/ferriskey-cli-core/src/client.rs b/libs/ferriskey-cli-core/src/client.rs index 1310435..84079c4 100644 --- a/libs/ferriskey-cli-core/src/client.rs +++ b/libs/ferriskey-cli-core/src/client.rs @@ -8,6 +8,7 @@ use ferriskey_cli_commands::{ use serde::Serialize; use thiserror::Error; +use crate::confirm::{self, confirm}; use crate::config::{ConfigError, ContextStore, FileContextRepository, StoredContext}; use crate::session::{self, SessionError}; @@ -43,6 +44,8 @@ pub enum ClientCommandError { Api(#[from] FerriskeyClientError), #[error(transparent)] Session(#[from] SessionError), + #[error(transparent)] + Confirm(#[from] confirm::ConfirmError), #[error("context '{0}' does not exist")] ContextNotFound(String), #[error("client '{0}' not found")] @@ -107,6 +110,10 @@ fn delete_client( inline_context: Option, args: ClientDeleteArgs, ) -> Result<()> { + confirm( + &format!("Delete client '{}'?", args.client_id), + args.force, + )?; let context = resolve_context(context_override, inline_context)?; let realm = resolve_realm(&context, args.realm.clone())?; let client = session::authenticated_client(&context, &realm)?; diff --git a/libs/ferriskey-cli-core/src/confirm.rs b/libs/ferriskey-cli-core/src/confirm.rs new file mode 100644 index 0000000..f48e97d --- /dev/null +++ b/libs/ferriskey-cli-core/src/confirm.rs @@ -0,0 +1,43 @@ +use std::io::{self, IsTerminal, Write}; + +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ConfirmError { + #[error("aborted")] + Aborted, + #[error( + "refusing to proceed without confirmation: re-run with --force to confirm in a non-interactive shell" + )] + NonInteractive, + #[error("failed to read confirmation from stdin")] + Io(#[source] io::Error), +} + +/// Prompt for confirmation of a destructive action. +/// +/// - When `force` is true, returns `Ok(())` immediately (no prompt). +/// - When stdin is not a TTY, returns `NonInteractive` so scripts must opt in +/// with `--force` rather than hanging or silently proceeding. +/// - Otherwise prompts on stderr and accepts `y`/`yes` (case-insensitive). +pub(crate) fn confirm(prompt: &str, force: bool) -> Result<(), ConfirmError> { + if force { + return Ok(()); + } + if !io::stdin().is_terminal() { + return Err(ConfirmError::NonInteractive); + } + + eprint!("{prompt} [y/N]: "); + let _ = io::stderr().flush(); + + let mut input = String::new(); + io::stdin() + .read_line(&mut input) + .map_err(ConfirmError::Io)?; + + match input.trim().to_ascii_lowercase().as_str() { + "y" | "yes" => Ok(()), + _ => Err(ConfirmError::Aborted), + } +} diff --git a/libs/ferriskey-cli-core/src/credentials.rs b/libs/ferriskey-cli-core/src/credentials.rs index e43d4d1..94d5bf3 100644 --- a/libs/ferriskey-cli-core/src/credentials.rs +++ b/libs/ferriskey-cli-core/src/credentials.rs @@ -147,6 +147,19 @@ impl CredentialsRepository { Ok(()) } + /// Remove the stored credentials file. Returns `Ok(false)` when there was + /// nothing to remove (no active session), `Ok(true)` when a file was deleted. + pub fn delete(&self) -> Result { + match fs::remove_file(&self.file_path) { + Ok(()) => Ok(true), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false), + Err(source) => Err(CredentialsError::Write { + path: self.file_path.display().to_string(), + source, + }), + } + } + fn ensure_parent_dir(&self) -> Result<()> { if let Some(parent) = self.file_path.parent() { fs::create_dir_all(parent).map_err(|source| CredentialsError::CreateDirectory { diff --git a/libs/ferriskey-cli-core/src/lib.rs b/libs/ferriskey-cli-core/src/lib.rs index 14f4521..8e609ab 100644 --- a/libs/ferriskey-cli-core/src/lib.rs +++ b/libs/ferriskey-cli-core/src/lib.rs @@ -1,5 +1,6 @@ mod auth; mod client; +mod confirm; mod config; mod context; mod credentials; @@ -73,6 +74,7 @@ pub fn run(cli: Cli) -> Result<()> { cli.realm.as_deref(), command, )?), + Commands::Logout => Ok(auth::logout()?), } } diff --git a/libs/ferriskey-cli-core/src/realm.rs b/libs/ferriskey-cli-core/src/realm.rs index 7225b31..e6dd1e0 100644 --- a/libs/ferriskey-cli-core/src/realm.rs +++ b/libs/ferriskey-cli-core/src/realm.rs @@ -1,8 +1,11 @@ use ferriskey_cli_client::{CreateRealmRequest, FerriskeyClient, FerriskeyClientError, Realm}; -use ferriskey_cli_commands::{RealmCommand, RealmImportArgs, RealmNameArgs, RealmSubcommand}; +use ferriskey_cli_commands::{ + RealmCommand, RealmDeleteArgs, RealmImportArgs, RealmNameArgs, RealmSubcommand, +}; use serde::Serialize; use thiserror::Error; +use crate::confirm::{self, confirm}; use crate::config::{ConfigError, FileContextRepository, StoredContext}; use crate::import::{self, ImportReport, RealmBlueprint}; use crate::session::{self, SessionError}; @@ -44,6 +47,8 @@ pub enum RealmCommandError { Api(#[from] FerriskeyClientError), #[error(transparent)] Session(#[from] SessionError), + #[error(transparent)] + Confirm(#[from] confirm::ConfirmError), #[error("context '{0}' does not exist")] ContextNotFound(String), #[error("no active context is configured")] @@ -159,8 +164,15 @@ fn delete_realm( output_format: &str, context_override: Option<&str>, inline_context: Option, - args: RealmNameArgs, + args: RealmDeleteArgs, ) -> Result<()> { + confirm( + &format!( + "Delete realm '{}'? This permanently removes its clients, users, and roles.", + args.name + ), + args.force, + )?; let context = resolve_context(context_override, inline_context)?; let client = auth_client(&context)?; client.delete_realm(&args.name)?; @@ -262,24 +274,19 @@ fn render_reports(output_format: &str, reports: &[ImportReport]) -> Result<()> { } } -/// Serializes a slice as JSON — a single element is emitted as an object, more -/// than one as an array. +/// Serializes a collection as a JSON array. `realm import` can resolve one or +/// many realms, so the shape is kept stable (always an array) for scripting, +/// regardless of how many were resolved. fn render_json(items: &[T]) -> Result<()> { - let json = match items { - [single] => serde_json::to_string_pretty(single), - _ => serde_json::to_string_pretty(items), - } - .map_err(|source| RealmCommandError::SerializeJson { source })?; + let json = serde_json::to_string_pretty(items) + .map_err(|source| RealmCommandError::SerializeJson { source })?; println!("{json}"); Ok(()) } fn render_yaml(items: &[T]) -> Result<()> { - let yaml = match items { - [single] => serde_yaml::to_string(single), - _ => serde_yaml::to_string(items), - } - .map_err(|source| RealmCommandError::SerializeYaml { source })?; + let yaml = + serde_yaml::to_string(items).map_err(|source| RealmCommandError::SerializeYaml { source })?; println!("{yaml}"); Ok(()) } diff --git a/libs/ferriskey-cli-core/src/session.rs b/libs/ferriskey-cli-core/src/session.rs index e7d2a00..4abfbfc 100644 --- a/libs/ferriskey-cli-core/src/session.rs +++ b/libs/ferriskey-cli-core/src/session.rs @@ -1,14 +1,22 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + use ferriskey_cli_client::{FerriskeyClient, FerriskeyClientError}; use thiserror::Error; use crate::config::StoredContext; -use crate::credentials::{CredentialsError, CredentialsRepository}; +use crate::credentials::{CredentialsError, CredentialsRepository, StoredCredentials}; + +/// Tokens are refreshed this many seconds before their nominal expiry to absorb +/// clock skew and request latency. +const EXPIRY_LEEWAY_SECONDS: i64 = 30; /// Authentication source used to obtain the bearer token. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AuthSource { - /// Reused the access token persisted by `ferris-ctl login`. + /// Reused the still-valid access token persisted by `ferris-ctl login`. StoredToken, + /// Refreshed an expired access token using the stored refresh token. + RefreshedToken, /// Exchanged the context's client_id/client_secret via the /// `client_credentials` OAuth grant. ClientCredentials, @@ -24,13 +32,16 @@ pub enum SessionError { "no credentials available: run 'ferris-ctl login' or add a context with --client-secret" )] NoCredentials, + #[error("system time is before the unix epoch")] + SystemTime, } /// Resolve a bearer token for the given context and target realm. /// /// Resolution order: -/// 1. If `credentials.toml` exists and its (server_url, realm, client_id) -/// triple matches the context+realm, reuse its access_token. +/// 1. If `credentials.toml` matches the context+realm triple, reuse its +/// access_token while still valid, otherwise refresh it via the stored +/// refresh_token and persist the rotated tokens. /// 2. Else, if the context carries a non-empty client_secret, perform the /// `client_credentials` OAuth grant. /// 3. Else, fail loudly with `NoCredentials` — the user needs to log in. @@ -38,12 +49,20 @@ pub(crate) fn resolve_bearer_token( context: &StoredContext, realm: &str, ) -> Result<(String, AuthSource), SessionError> { - if let Some(creds) = CredentialsRepository::new()?.load()? + let repository = CredentialsRepository::new()?; + if let Some(creds) = repository.load()? && creds.server_url == context.url && creds.realm == realm && creds.client_id == context.client_id { - return Ok((creds.access_token, AuthSource::StoredToken)); + if !is_expired(creds.obtained_at, creds.expires_in, unix_now()?) { + return Ok((creds.access_token, AuthSource::StoredToken)); + } + // Access token expired: try to refresh it silently. On failure we fall + // through to the client_secret path or surface NoCredentials. + if let Some(access_token) = try_refresh(context, realm, &creds, &repository)? { + return Ok((access_token, AuthSource::RefreshedToken)); + } } if let Some(secret) = context.client_secret.as_deref().filter(|s| !s.is_empty()) { @@ -55,6 +74,55 @@ pub(crate) fn resolve_bearer_token( Err(SessionError::NoCredentials) } +/// Returns true when a token obtained at `obtained_at` with lifetime +/// `expires_in` seconds is at or past its expiry (minus a leeway buffer). +fn is_expired(obtained_at: i64, expires_in: i64, now: i64) -> bool { + obtained_at + expires_in - EXPIRY_LEEWAY_SECONDS <= now +} + +/// Attempt to refresh the stored credentials. Returns the new access token on +/// success, or `Ok(None)` when refresh is not possible (refresh token expired) +/// or rejected by the server — letting the caller fall back gracefully. +fn try_refresh( + context: &StoredContext, + realm: &str, + creds: &StoredCredentials, + repository: &CredentialsRepository, +) -> Result, SessionError> { + // If we know the refresh token's lifetime and it has lapsed, don't bother. + if let Some(refresh_expires_in) = creds.refresh_expires_in + && is_expired(creds.obtained_at, refresh_expires_in, unix_now()?) + { + return Ok(None); + } + + let auth = FerriskeyClient::new(context.url.clone(), "", "")?; + let secret = context.client_secret.as_deref().filter(|s| !s.is_empty()); + let token = + match auth.exchange_refresh_token(realm, &context.client_id, &creds.refresh_token, secret) { + Ok(token) => token, + Err(_) => return Ok(None), + }; + + let updated = StoredCredentials::from_token( + creds.server_url.clone(), + creds.realm.clone(), + creds.client_id.clone(), + token, + unix_now()?, + ); + let access_token = updated.access_token.clone(); + repository.save(&updated)?; + Ok(Some(access_token)) +} + +fn unix_now() -> Result { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs() as i64) + .map_err(|_| SessionError::SystemTime) +} + /// Convenience wrapper that returns an authenticated `FerriskeyClient` ready /// to make API calls. pub(crate) fn authenticated_client( @@ -64,3 +132,26 @@ pub(crate) fn authenticated_client( let (token, _source) = resolve_bearer_token(context, realm)?; Ok(FerriskeyClient::new(context.url.clone(), "", token)?) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn fresh_token_is_not_expired() { + // obtained at t=1000, lives 300s → valid well before t=1000+300-leeway. + assert!(!is_expired(1000, 300, 1100)); + } + + #[test] + fn token_past_lifetime_is_expired() { + assert!(is_expired(1000, 300, 1400)); + } + + #[test] + fn token_within_leeway_window_is_treated_as_expired() { + // Expiry is 1300; with a 30s leeway anything from 1270 onward refreshes. + assert!(is_expired(1000, 300, 1280)); + assert!(!is_expired(1000, 300, 1269)); + } +} diff --git a/libs/ferriskey-cli-core/src/user.rs b/libs/ferriskey-cli-core/src/user.rs index 3613c9a..554127f 100644 --- a/libs/ferriskey-cli-core/src/user.rs +++ b/libs/ferriskey-cli-core/src/user.rs @@ -7,6 +7,7 @@ use ferriskey_cli_commands::{ use serde::Serialize; use thiserror::Error; +use crate::confirm::{self, confirm}; use crate::config::{ConfigError, FileContextRepository, StoredContext}; use crate::session::{self, SessionError}; @@ -42,6 +43,8 @@ pub enum UserCommandError { Api(#[from] FerriskeyClientError), #[error(transparent)] Session(#[from] SessionError), + #[error(transparent)] + Confirm(#[from] confirm::ConfirmError), #[error("context '{0}' does not exist")] ContextNotFound(String), #[error("no active context is configured")] @@ -181,6 +184,10 @@ fn delete_user( inline_context: Option, args: UserDeleteArgs, ) -> Result<()> { + confirm( + &format!("Delete user '{}'?", args.username), + args.force, + )?; let context = resolve_context(context_override, inline_context)?; let realm = resolve_realm(&context, args.realm)?; let client = authenticate(&context, &realm)?; From 7cdd47f2607caa40384750fca9bf433c210ca6bf Mon Sep 17 00:00:00 2001 From: Nathael Bonnal Date: Sat, 13 Jun 2026 05:26:55 +0200 Subject: [PATCH 2/3] fix(ci): build release binaries with rustls only so all targets compile --- .github/workflows/release.yaml | 4 + Cargo.lock | 284 --------------------------- libs/ferriskey-cli-client/Cargo.toml | 2 +- libs/ferriskey-cli-core/Cargo.toml | 2 +- 4 files changed, 6 insertions(+), 286 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index cb89db4..f6f5f87 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -100,6 +100,10 @@ jobs: release: name: github release needs: build + # Publish the release as long as the run wasn't cancelled, even if one + # build target failed — a release with the binaries that did build beats + # no release at all. Missing assets are visible in the Actions run. + if: ${{ !cancelled() }} runs-on: ubuntu-latest steps: - name: checkout diff --git a/Cargo.lock b/Cargo.lock index ca4591c..d0eb096 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -165,32 +165,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - [[package]] name = "ctrlc" version = "3.5.2" @@ -225,15 +199,6 @@ dependencies = [ "syn", ] -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -305,33 +270,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -430,25 +374,6 @@ dependencies = [ "wasip3", ] -[[package]] -name = "h2" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "hashbrown" version = "0.15.5" @@ -519,7 +444,6 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2", "http", "http-body", "httparse", @@ -548,22 +472,6 @@ dependencies = [ "webpki-roots", ] -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - [[package]] name = "hyper-util" version = "0.1.20" @@ -582,11 +490,9 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", - "system-configuration", "tokio", "tower-service", "tracing", - "windows-registry", ] [[package]] @@ -789,12 +695,6 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - [[package]] name = "mio" version = "1.1.1" @@ -806,23 +706,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "native-tls" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "nix" version = "0.31.2" @@ -862,50 +745,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" -[[package]] -name = "openssl" -version = "0.10.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" -dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "openssl-probe" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" - -[[package]] -name = "openssl-sys" -version = "0.9.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "percent-encoding" version = "2.3.2" @@ -924,12 +763,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - [[package]] name = "potential_utf" version = "0.1.4" @@ -1080,22 +913,17 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64", "bytes", - "encoding_rs", "futures-channel", "futures-core", "futures-util", - "h2", "http", "http-body", "http-body-util", "hyper", "hyper-rustls", - "hyper-tls", "hyper-util", "js-sys", "log", - "mime", - "native-tls", "percent-encoding", "pin-project-lite", "quinn", @@ -1106,7 +934,6 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-native-tls", "tokio-rustls", "tower", "tower-http", @@ -1198,38 +1025,6 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" -[[package]] -name = "schannel" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "security-framework" -version = "3.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" -dependencies = [ - "bitflags", - "core-foundation 0.10.1", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "semver" version = "1.0.27" @@ -1390,27 +1185,6 @@ dependencies = [ "syn", ] -[[package]] -name = "system-configuration" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" -dependencies = [ - "bitflags", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "tempfile" version = "3.26.0" @@ -1483,16 +1257,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.4" @@ -1503,19 +1267,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - [[package]] name = "toml" version = "0.8.23" @@ -1675,12 +1426,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - [[package]] name = "want" version = "0.3.1" @@ -1842,35 +1587,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - [[package]] name = "windows-sys" version = "0.52.0" diff --git a/libs/ferriskey-cli-client/Cargo.toml b/libs/ferriskey-cli-client/Cargo.toml index 4a44322..8c8887a 100644 --- a/libs/ferriskey-cli-client/Cargo.toml +++ b/libs/ferriskey-cli-client/Cargo.toml @@ -9,7 +9,7 @@ homepage.workspace = true description = "Rust SDK for the FerrisKey API" [dependencies] -reqwest = { version = "0.12", features = ["blocking", "json", "rustls-tls"] } +reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "2.0" diff --git a/libs/ferriskey-cli-core/Cargo.toml b/libs/ferriskey-cli-core/Cargo.toml index 26a250e..b98098a 100644 --- a/libs/ferriskey-cli-core/Cargo.toml +++ b/libs/ferriskey-cli-core/Cargo.toml @@ -16,7 +16,7 @@ base64 = "0.22" ctrlc = "3.4" ferriskey-cli-client = { path = "../ferriskey-cli-client", version = "0.2.0" } ferriskey-cli-commands = { path = "../ferriskey-cli-commands", version = "0.2.0" } -reqwest = { version = "0.12", features = ["blocking", "json", "rustls-tls"] } +reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_yaml = "0.9" From d5313abecedd594e74f7fc5c2408d0ea792804af Mon Sep 17 00:00:00 2001 From: Nathael Bonnal Date: Sat, 13 Jun 2026 05:32:43 +0200 Subject: [PATCH 3/3] feat: bump version to 0.2.2 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 7dbfb36..4156f83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ members = ["libs/ferriskey-cli-client", "libs/ferriskey-cli-commands", "libs/fer resolver = "2" [workspace.package] -version = "0.2.1" +version = "0.2.2" authors = ["nathaelb "] edition = "2024" license = "Apache-2.0"