codex: address create-api-key review feedback

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Michael Fan
2026-03-25 18:52:43 -04:00
parent 285c4926b5
commit ec0ca67f78
4 changed files with 91 additions and 44 deletions

View File

@@ -313,23 +313,29 @@ async fn wait_for_default_project(
let deadline = std::time::Instant::now() + Duration::from_secs(timeout_seconds); let deadline = std::time::Instant::now() + Duration::from_secs(timeout_seconds);
loop { loop {
let organizations = list_organizations(client, api_base, session_key).await?; let organizations = list_organizations(client, api_base, session_key).await?;
let last_state = if let Some(organization) = select_active_organization(&organizations) { let last_state = if organizations.is_empty() {
let projects = list_projects(client, api_base, session_key, &organization.id).await?; "no organization found".to_string()
if let Some(project) = find_default_project(&projects) { } else {
return Ok(ProvisioningTarget { let ordered_organizations = organizations_by_preference(&organizations);
organization_id: organization.id.clone(), let mut project_count = 0;
organization_title: organization.title.clone(), for organization in ordered_organizations {
project_id: project.id.clone(), let projects =
project_title: project.title.clone(), list_projects(client, api_base, session_key, &organization.id).await?;
}); project_count += projects.len();
if let Some(project) = find_default_project(&projects) {
return Ok(ProvisioningTarget {
organization_id: organization.id.clone(),
organization_title: organization.title.clone(),
project_id: project.id.clone(),
project_title: project.title.clone(),
});
}
} }
format!( format!(
"organization `{}` exists, but no default project is ready yet (saw {} projects).", "checked {} organizations and {} projects, but no default project is ready yet.",
organization.id, organizations.len(),
projects.len() project_count
) )
} else {
"no organization found".to_string()
}; };
if std::time::Instant::now() >= deadline { if std::time::Instant::now() >= deadline {
@@ -345,16 +351,22 @@ async fn wait_for_default_project(
} }
} }
fn select_active_organization(organizations: &[Organization]) -> Option<&Organization> { fn organizations_by_preference(organizations: &[Organization]) -> Vec<&Organization> {
organizations let mut ordered_organizations = organizations.iter().enumerate().collect::<Vec<_>>();
.iter() ordered_organizations.sort_by_key(|(index, organization)| {
.find(|organization| organization.is_default) let rank = if organization.is_default {
.or_else(|| { 0
organizations } else if organization.personal {
.iter() 1
.find(|organization| organization.personal) } else {
}) 2
.or_else(|| organizations.first()) };
(rank, *index)
});
ordered_organizations
.into_iter()
.map(|(_, organization)| organization)
.collect()
} }
fn find_default_project(projects: &[Project]) -> Option<&Project> { fn find_default_project(projects: &[Project]) -> Option<&Project> {

View File

@@ -11,7 +11,7 @@ use wiremock::matchers::path;
use wiremock::matchers::query_param; use wiremock::matchers::query_param;
#[test] #[test]
fn select_active_organization_prefers_default_then_personal_then_first() { fn organizations_by_preference_orders_default_then_personal_then_input_order() {
let organizations = vec![ let organizations = vec![
Organization { Organization {
id: "org-first".to_string(), id: "org-first".to_string(),
@@ -33,9 +33,12 @@ fn select_active_organization_prefers_default_then_personal_then_first() {
}, },
]; ];
let selected = select_active_organization(&organizations); let selected = organizations_by_preference(&organizations);
assert_eq!(selected, organizations.get(2)); assert_eq!(
selected,
vec![&organizations[2], &organizations[1], &organizations[0]]
);
} }
#[test] #[test]
@@ -97,6 +100,10 @@ async fn create_api_key_from_authorization_code_creates_api_key() {
"id": "org-default", "id": "org-default",
"title": "Default Org", "title": "Default Org",
"is_default": true, "is_default": true,
},
{
"id": "org-secondary",
"title": "Secondary Org",
} }
] ]
}))) })))
@@ -106,6 +113,15 @@ async fn create_api_key_from_authorization_code_creates_api_key() {
.and(path("/dashboard/organizations/org-default/projects")) .and(path("/dashboard/organizations/org-default/projects"))
.and(query_param("detail", "basic")) .and(query_param("detail", "basic"))
.and(query_param("limit", "100")) .and(query_param("limit", "100"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"data": []
})))
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/dashboard/organizations/org-secondary/projects"))
.and(query_param("detail", "basic"))
.and(query_param("limit", "100"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({ .respond_with(ResponseTemplate::new(200).set_body_json(json!({
"data": [ "data": [
{ {
@@ -119,7 +135,7 @@ async fn create_api_key_from_authorization_code_creates_api_key() {
.await; .await;
Mock::given(method("POST")) Mock::given(method("POST"))
.and(path( .and(path(
"/dashboard/organizations/org-default/projects/proj-default/api_keys", "/dashboard/organizations/org-secondary/projects/proj-default/api_keys",
)) ))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({ .respond_with(ResponseTemplate::new(200).set_body_json(json!({
"key": { "key": {
@@ -156,8 +172,8 @@ async fn create_api_key_from_authorization_code_creates_api_key() {
assert_eq!( assert_eq!(
output, output,
CreatedApiKey { CreatedApiKey {
organization_id: "org-default".to_string(), organization_id: "org-secondary".to_string(),
organization_title: Some("Default Org".to_string()), organization_title: Some("Secondary Org".to_string()),
default_project_id: "proj-default".to_string(), default_project_id: "proj-default".to_string(),
default_project_title: Some("Default Project".to_string()), default_project_title: Some("Default Project".to_string()),
project_api_key: "sk-proj-123".to_string(), project_api_key: "sk-proj-123".to_string(),

View File

@@ -17,10 +17,6 @@ pub use server::LoginServer;
pub use server::ServerOptions; pub use server::ServerOptions;
pub use server::run_login_server; pub use server::run_login_server;
pub use create_api_key::CreateApiKeyError;
pub use create_api_key::CreatedApiKey;
pub use create_api_key::PendingCreateApiKey;
pub use create_api_key::start_create_api_key;
pub use auth::AuthConfig; pub use auth::AuthConfig;
pub use auth::AuthCredentialsStoreMode; pub use auth::AuthCredentialsStoreMode;
pub use auth::AuthDotJson; pub use auth::AuthDotJson;
@@ -40,4 +36,8 @@ pub use auth::logout;
pub use auth::read_openai_api_key_from_env; pub use auth::read_openai_api_key_from_env;
pub use auth::save_auth; pub use auth::save_auth;
pub use codex_app_server_protocol::AuthMode; pub use codex_app_server_protocol::AuthMode;
pub use create_api_key::CreateApiKeyError;
pub use create_api_key::CreatedApiKey;
pub use create_api_key::PendingCreateApiKey;
pub use create_api_key::start_create_api_key;
pub use token_data::TokenData; pub use token_data::TokenData;

View File

@@ -385,16 +385,14 @@ fn process_authorization_code_request(
parsed_url.query_pairs().into_owned().collect(); parsed_url.query_pairs().into_owned().collect();
if params.get("state").map(String::as_str) != Some(expected_state) { if params.get("state").map(String::as_str) != Some(expected_state) {
return HandledRequest::ResponseAndExit { let mut response = Response::from_string(
status: StatusCode(400), "<h1>State mismatch</h1><p>Return to your terminal and try again.</p>",
headers: html_headers(), )
body: b"<h1>State mismatch</h1><p>Return to your terminal and try again.</p>" .with_status_code(400);
.to_vec(), if let Some(header) = html_headers().into_iter().next() {
result: Err(io::Error::new( response = response.with_header(header);
io::ErrorKind::PermissionDenied, }
"State mismatch in OAuth callback.", return HandledRequest::Response(response);
)),
};
} }
if let Some(error_code) = params.get("error") { if let Some(error_code) = params.get("error") {
@@ -451,3 +449,24 @@ fn authorization_code_error_message(error_code: &str, error_description: Option<
format!("Authentication failed: {error_code}") format!("Authentication failed: {error_code}")
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn process_authorization_code_request_keeps_server_running_on_state_mismatch() {
let response = process_authorization_code_request(
"/auth/callback?state=wrong-state&code=auth-code",
"/auth/callback",
"expected-state",
);
match response {
HandledRequest::Response(_) => {}
HandledRequest::RedirectWithHeader(_) | HandledRequest::ResponseAndExit { .. } => {
panic!("state mismatch should return a response without exiting")
}
}
}
}