Make `uv auth dir` service-aware (#15649)

## Summary

This got lost when https://github.com/astral-sh/uv/pull/15637 was merged
into not-`main`.
This commit is contained in:
Charlie Marsh 2025-09-02 22:58:59 -04:00 committed by GitHub
parent 70cb0df7c2
commit ad35d120d6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 95 additions and 48 deletions

View File

@ -1,5 +1,5 @@
use std::io; use std::io;
use std::path::PathBuf; use std::path::{Path, PathBuf};
use std::time::Duration; use std::time::Duration;
use base64::Engine; use base64::Engine;
@ -92,26 +92,37 @@ impl From<AccessToken> for Credentials {
/// The default tolerance for the access token expiration. /// The default tolerance for the access token expiration.
pub const DEFAULT_TOLERANCE_SECS: u64 = 60 * 5; pub const DEFAULT_TOLERANCE_SECS: u64 = 60 * 5;
/// The root directory for the pyx token store. #[derive(Debug, Clone)]
fn root_dir(api: &DisplaySafeUrl) -> Result<PathBuf, io::Error> { struct PyxDirectories {
/// The root directory for the token store (e.g., `/Users/ferris/.local/share/pyx/credentials`).
root: PathBuf,
/// The subdirectory for the token store (e.g., `/Users/ferris/.local/share/uv/credentials/3859a629b26fda96`).
subdirectory: PathBuf,
}
impl PyxDirectories {
/// Detect the [`PyxDirectories`] for a given API URL.
fn from_api(api: &DisplaySafeUrl) -> Result<Self, io::Error> {
// Store credentials in a subdirectory based on the API URL. // Store credentials in a subdirectory based on the API URL.
let digest = uv_cache_key::cache_digest(&CanonicalUrl::new(api)); let digest = uv_cache_key::cache_digest(&CanonicalUrl::new(api));
// If the user explicitly set `PYX_CREDENTIALS_DIR`, use that. // If the user explicitly set `PYX_CREDENTIALS_DIR`, use that.
if let Some(tool_dir) = std::env::var_os(EnvVars::PYX_CREDENTIALS_DIR) { if let Some(root) = std::env::var_os(EnvVars::PYX_CREDENTIALS_DIR) {
return std::path::absolute(tool_dir).map(|dir| dir.join(&digest)); let root = std::path::absolute(root)?;
let subdirectory = root.join(&digest);
return Ok(Self { root, subdirectory });
} }
// If the user has pyx credentials in their uv credentials directory, read them for // If the user has pyx credentials in their uv credentials directory, read them for
// backwards compatibility. // backwards compatibility.
let credentials_dir = if let Some(tool_dir) = std::env::var_os(EnvVars::UV_CREDENTIALS_DIR) { let root = if let Some(tool_dir) = std::env::var_os(EnvVars::UV_CREDENTIALS_DIR) {
std::path::absolute(tool_dir)? std::path::absolute(tool_dir)?
} else { } else {
StateStore::from_settings(None)?.bucket(StateBucket::Credentials) StateStore::from_settings(None)?.bucket(StateBucket::Credentials)
}; };
let credentials_dir = credentials_dir.join(&digest); let subdirectory = root.join(&digest);
if credentials_dir.exists() { if subdirectory.exists() {
return Ok(credentials_dir); return Ok(Self { root, subdirectory });
} }
// Otherwise, use (e.g.) `~/.local/share/pyx`. // Otherwise, use (e.g.) `~/.local/share/pyx`.
@ -122,13 +133,18 @@ fn root_dir(api: &DisplaySafeUrl) -> Result<PathBuf, io::Error> {
)); ));
}; };
Ok(xdg.data_dir().join("pyx").join("credentials").join(&digest)) let root = xdg.data_dir().join("pyx").join("credentials");
let subdirectory = root.join(&digest);
Ok(Self { root, subdirectory })
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PyxTokenStore { pub struct PyxTokenStore {
/// The root directory for the token store (e.g., `/Users/ferris/.local/share/pyx/credentials/3859a629b26fda96`). /// The root directory for the token store (e.g., `/Users/ferris/.local/share/pyx/credentials`).
root: PathBuf, root: PathBuf,
/// The subdirectory for the token store (e.g., `/Users/ferris/.local/share/uv/credentials/3859a629b26fda96`).
subdirectory: PathBuf,
/// The API URL for the token store (e.g., `https://api.pyx.dev`). /// The API URL for the token store (e.g., `https://api.pyx.dev`).
api: DisplaySafeUrl, api: DisplaySafeUrl,
/// The CDN domain for the token store (e.g., `astralhosted.com`). /// The CDN domain for the token store (e.g., `astralhosted.com`).
@ -151,9 +167,19 @@ impl PyxTokenStore {
.unwrap_or_else(|| SmallString::from(arcstr::literal!("astralhosted.com"))); .unwrap_or_else(|| SmallString::from(arcstr::literal!("astralhosted.com")));
// Determine the root directory for the token store. // Determine the root directory for the token store.
let root = root_dir(&api)?; let PyxDirectories { root, subdirectory } = PyxDirectories::from_api(&api)?;
Ok(Self { root, api, cdn }) Ok(Self {
root,
subdirectory,
api,
cdn,
})
}
/// Return the root directory for the token store.
pub fn root(&self) -> &Path {
&self.root
} }
/// Return the API URL for the token store. /// Return the API URL for the token store.
@ -212,18 +238,21 @@ impl PyxTokenStore {
/// Write the tokens to the store. /// Write the tokens to the store.
pub async fn write(&self, tokens: &PyxTokens) -> Result<(), TokenStoreError> { pub async fn write(&self, tokens: &PyxTokens) -> Result<(), TokenStoreError> {
fs_err::tokio::create_dir_all(&self.root).await?; fs_err::tokio::create_dir_all(&self.subdirectory).await?;
match tokens { match tokens {
PyxTokens::OAuth(tokens) => { PyxTokens::OAuth(tokens) => {
// Write OAuth tokens to a generic `tokens.json` file. // Write OAuth tokens to a generic `tokens.json` file.
fs_err::tokio::write(self.root.join("tokens.json"), serde_json::to_vec(tokens)?) fs_err::tokio::write(
self.subdirectory.join("tokens.json"),
serde_json::to_vec(tokens)?,
)
.await?; .await?;
} }
PyxTokens::ApiKey(tokens) => { PyxTokens::ApiKey(tokens) => {
// Write API key tokens to a file based on the API key. // Write API key tokens to a file based on the API key.
let digest = uv_cache_key::cache_digest(&tokens.api_key); let digest = uv_cache_key::cache_digest(&tokens.api_key);
fs_err::tokio::write( fs_err::tokio::write(
self.root.join(format!("{digest}.json")), self.subdirectory.join(format!("{digest}.json")),
&tokens.access_token, &tokens.access_token,
) )
.await?; .await?;
@ -236,7 +265,7 @@ impl PyxTokenStore {
pub fn has_credentials(&self) -> bool { pub fn has_credentials(&self) -> bool {
read_pyx_auth_token().is_some() read_pyx_auth_token().is_some()
|| read_pyx_api_key().is_some() || read_pyx_api_key().is_some()
|| self.root.join("tokens.json").is_file() || self.subdirectory.join("tokens.json").is_file()
} }
/// Read the tokens from the store. /// Read the tokens from the store.
@ -245,7 +274,7 @@ impl PyxTokenStore {
if let Some(api_key) = read_pyx_api_key() { if let Some(api_key) = read_pyx_api_key() {
// Read the API key tokens from a file based on the API key. // Read the API key tokens from a file based on the API key.
let digest = uv_cache_key::cache_digest(&api_key); let digest = uv_cache_key::cache_digest(&api_key);
match fs_err::tokio::read(self.root.join(format!("{digest}.json"))).await { match fs_err::tokio::read(self.subdirectory.join(format!("{digest}.json"))).await {
Ok(data) => { Ok(data) => {
let access_token = let access_token =
AccessToken::from(String::from_utf8(data).expect("Invalid UTF-8")); AccessToken::from(String::from_utf8(data).expect("Invalid UTF-8"));
@ -258,7 +287,7 @@ impl PyxTokenStore {
Err(err) => Err(err.into()), Err(err) => Err(err.into()),
} }
} else { } else {
match fs_err::tokio::read(self.root.join("tokens.json")).await { match fs_err::tokio::read(self.subdirectory.join("tokens.json")).await {
Ok(data) => { Ok(data) => {
let tokens: PyxOAuthTokens = serde_json::from_slice(&data)?; let tokens: PyxOAuthTokens = serde_json::from_slice(&data)?;
Ok(Some(PyxTokens::OAuth(tokens))) Ok(Some(PyxTokens::OAuth(tokens)))
@ -271,7 +300,7 @@ impl PyxTokenStore {
/// Remove the tokens from the store. /// Remove the tokens from the store.
pub async fn delete(&self) -> Result<(), io::Error> { pub async fn delete(&self) -> Result<(), io::Error> {
fs_err::tokio::remove_dir_all(&self.root).await?; fs_err::tokio::remove_dir_all(&self.subdirectory).await?;
Ok(()) Ok(())
} }

View File

@ -4418,7 +4418,7 @@ pub enum AuthCommand {
/// ///
/// Credentials are only stored in this directory when the plaintext backend is used, as /// Credentials are only stored in this directory when the plaintext backend is used, as
/// opposed to the native backend, which uses the system keyring. /// opposed to the native backend, which uses the system keyring.
Dir, Dir(AuthDirArgs),
} }
#[derive(Args)] #[derive(Args)]
@ -5610,6 +5610,12 @@ pub struct AuthTokenArgs {
pub keyring_provider: Option<KeyringProviderType>, pub keyring_provider: Option<KeyringProviderType>,
} }
#[derive(Args)]
pub struct AuthDirArgs {
/// The service to lookup.
pub service: Option<Service>,
}
#[derive(Args)] #[derive(Args)]
pub struct GenerateShellCompletionArgs { pub struct GenerateShellCompletionArgs {
/// The shell to generate the completion script for /// The shell to generate the completion script for

View File

@ -1,13 +1,20 @@
use anstream::println; use anstream::println;
use owo_colors::OwoColorize; use owo_colors::OwoColorize;
use uv_auth::TextCredentialStore; use uv_auth::{PyxTokenStore, Service, TextCredentialStore};
use uv_fs::Simplified; use uv_fs::Simplified;
/// Show the credentials directory. /// Show the credentials directory.
pub(crate) fn dir() -> anyhow::Result<()> { pub(crate) fn dir(service: Option<&Service>) -> anyhow::Result<()> {
if let Some(service) = service {
let pyx_store = PyxTokenStore::from_settings()?;
if pyx_store.is_known_domain(service.url()) {
println!("{}", pyx_store.root().simplified_display().cyan());
return Ok(());
}
}
let root = TextCredentialStore::directory_path()?; let root = TextCredentialStore::directory_path()?;
println!("{}", root.simplified_display().cyan()); println!("{}", root.simplified_display().cyan());
Ok(()) Ok(())
} }

View File

@ -502,9 +502,9 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
.await .await
} }
Commands::Auth(AuthNamespace { Commands::Auth(AuthNamespace {
command: AuthCommand::Dir, command: AuthCommand::Dir(args),
}) => { }) => {
commands::auth_dir()?; commands::auth_dir(args.service.as_ref())?;
Ok(ExitStatus::Success) Ok(ExitStatus::Success)
} }
Commands::Help(args) => commands::help( Commands::Help(args) => commands::help(

View File

@ -279,9 +279,14 @@ Credentials are only stored in this directory when the plaintext backend is used
<h3 class="cli-reference">Usage</h3> <h3 class="cli-reference">Usage</h3>
``` ```
uv auth dir [OPTIONS] uv auth dir [OPTIONS] [SERVICE]
``` ```
<h3 class="cli-reference">Arguments</h3>
<dl class="cli-reference"><dt id="uv-auth-dir--service"><a href="#uv-auth-dir--service"<code>SERVICE</code></a></dt><dd><p>The service to lookup</p>
</dd></dl>
<h3 class="cli-reference">Options</h3> <h3 class="cli-reference">Options</h3>
<dl class="cli-reference"><dt id="uv-auth-dir--allow-insecure-host"><a href="#uv-auth-dir--allow-insecure-host"><code>--allow-insecure-host</code></a>, <code>--trusted-host</code> <i>allow-insecure-host</i></dt><dd><p>Allow insecure connections to a host.</p> <dl class="cli-reference"><dt id="uv-auth-dir--allow-insecure-host"><a href="#uv-auth-dir--allow-insecure-host"><code>--allow-insecure-host</code></a>, <code>--trusted-host</code> <i>allow-insecure-host</i></dt><dd><p>Allow insecure connections to a host.</p>