auth: generalize ExternalAuthTokens for bearer-only refreshers

This commit is contained in:
Michael Bolin
2026-03-30 15:40:51 -07:00
parent d4cf2faae8
commit e953180be9
8 changed files with 340 additions and 62 deletions
+5 -5
View File
@@ -144,11 +144,11 @@ impl ExternalAuthRefresher for ExternalAuthRefreshBridge {
let response: ChatgptAuthTokensRefreshResponse =
serde_json::from_value(result).map_err(std::io::Error::other)?;
Ok(ExternalAuthTokens {
access_token: response.access_token,
chatgpt_account_id: response.chatgpt_account_id,
chatgpt_plan_type: response.chatgpt_plan_type,
})
Ok(ExternalAuthTokens::chatgpt(
response.access_token,
response.chatgpt_account_id,
response.chatgpt_plan_type,
))
}
}
+48 -8
View File
@@ -102,12 +102,12 @@ use crate::error::Result;
use crate::flags::CODEX_RS_SSE_FIXTURE;
use crate::model_provider_info::ModelProviderInfo;
use crate::model_provider_info::WireApi;
use crate::request_auth::RequestAuthContext;
use crate::request_auth::RequestUnauthorizedRecovery;
use crate::request_auth::ResolvedRequestAuth;
use crate::request_auth::UnauthorizedRecoveryError;
use crate::request_auth::UnauthorizedRecoveryExecution;
use crate::request_auth::UnauthorizedRecoveryOutcome;
use crate::request_auth::resolve_request_auth;
use crate::response_debug_context::extract_response_debug_context;
use crate::response_debug_context::extract_response_debug_context_from_api_error;
use crate::response_debug_context::telemetry_api_error_message;
@@ -135,7 +135,7 @@ pub(crate) const WEBSOCKET_CONNECT_TIMEOUT: Duration =
/// configuration is per turn and is passed explicitly to streaming/unary methods.
#[derive(Debug)]
struct ModelClientState {
auth_manager: Option<Arc<AuthManager>>,
request_auth: RequestAuthContext,
conversation_id: ThreadId,
provider: ModelProviderInfo,
auth_env_telemetry: AuthEnvTelemetry,
@@ -261,7 +261,10 @@ impl ModelClient {
let auth_env_telemetry = collect_auth_env_telemetry(&provider, codex_api_key_env_enabled);
Self {
state: Arc::new(ModelClientState {
auth_manager,
request_auth: RequestAuthContext::new(
auth_manager,
/*external_request_auth_source*/ None,
),
conversation_id,
provider,
auth_env_telemetry,
@@ -326,6 +329,13 @@ impl ModelClient {
activated
}
pub(crate) async fn resolve_provider_bearer_token(&self) -> Result<Option<String>> {
self.state
.request_auth
.resolve_external_request_auth_access_token()
.await
}
/// Compacts the current conversation history using the Compact endpoint.
///
/// This is a unary call (no streaming) that returns a new list of
@@ -518,7 +528,7 @@ impl ModelClient {
/// This centralizes setup used by both prewarm and normal request paths so they stay in
/// lockstep when auth/provider resolution changes.
async fn current_client_setup(&self) -> Result<ResolvedRequestAuth> {
resolve_request_auth(self.state.auth_manager.as_ref(), &self.state.provider).await
self.state.request_auth.resolve(&self.state.provider).await
}
/// Opens a websocket connection using the same header and telemetry wiring as normal turns.
@@ -997,13 +1007,15 @@ impl ModelClientSession {
return Ok(stream);
}
let mut unauthorized_recovery =
RequestUnauthorizedRecovery::new(self.client.state.auth_manager.as_ref());
let mut unauthorized_recovery = self.client.state.request_auth.unauthorized_recovery();
let mut pending_retry = PendingUnauthorizedRetry::default();
// Only loop after a successful auth-recovery step. Each retry must rebuild
// the client and request headers before issuing the same streaming request again.
loop {
let client_setup = self.client.current_client_setup().await?;
unauthorized_recovery.set_current_request_auth_access_token(
client_setup.request_auth_access_token.clone(),
);
let transport = ReqwestTransport::new(build_reqwest_client());
let request_auth_context = AuthRequestTelemetryContext::new(
client_setup.auth.as_ref().map(CodexAuth::auth_mode),
@@ -1085,13 +1097,15 @@ impl ModelClientSession {
warmup: bool,
request_trace: Option<W3cTraceContext>,
) -> Result<WebsocketStreamOutcome> {
let mut unauthorized_recovery =
RequestUnauthorizedRecovery::new(self.client.state.auth_manager.as_ref());
let mut unauthorized_recovery = self.client.state.request_auth.unauthorized_recovery();
let mut pending_retry = PendingUnauthorizedRetry::default();
// Only loop after a successful auth-recovery step. WebSocket auth is attached
// during connect, so a recovered token requires a fresh connection attempt.
loop {
let client_setup = self.client.current_client_setup().await?;
unauthorized_recovery.set_current_request_auth_access_token(
client_setup.request_auth_access_token.clone(),
);
let request_auth_context = AuthRequestTelemetryContext::new(
client_setup.auth.as_ref().map(CodexAuth::auth_mode),
&client_setup.api_auth,
@@ -1148,6 +1162,9 @@ impl ModelClientSession {
session_telemetry,
)
.await?;
if recovery.refreshes_request_auth {
self.reset_websocket_session();
}
pending_retry = PendingUnauthorizedRetry::from_recovery(recovery);
continue;
}
@@ -1628,6 +1645,29 @@ async fn handle_unauthorized(
Err(CodexErr::Io(other))
}
},
Err(UnauthorizedRecoveryError::RequestAuthSource { execution, error }) => {
session_telemetry.record_auth_recovery(
execution.mode,
execution.phase,
"recovery_failed_permanent",
debug.request_id.as_deref(),
debug.cf_ray.as_deref(),
debug.auth_error.as_deref(),
debug.auth_error_code.as_deref(),
/*recovery_reason*/ None,
/*auth_state_changed*/ None,
);
emit_feedback_auth_recovery_tags(
execution.mode,
execution.phase,
"recovery_failed_permanent",
debug.request_id.as_deref(),
debug.cf_ray.as_deref(),
debug.auth_error.as_deref(),
debug.auth_error_code.as_deref(),
);
Err(error)
}
}
}
+1
View File
@@ -111,6 +111,7 @@ fn auth_request_telemetry_context_tracks_attached_auth_and_retry_phase() {
mode: "managed",
phase: "refresh_token",
auth_state_changed: None,
refreshes_request_auth: false,
}),
);
+27 -10
View File
@@ -12,9 +12,8 @@ use crate::model_provider_info::ModelProviderInfo;
use crate::models_manager::collaboration_mode_presets::CollaborationModesConfig;
use crate::models_manager::collaboration_mode_presets::builtin_collaboration_mode_presets;
use crate::models_manager::model_info;
use crate::request_auth::RequestUnauthorizedRecovery;
use crate::request_auth::RequestAuthContext;
use crate::request_auth::UnauthorizedRecoveryOutcome;
use crate::request_auth::resolve_request_auth;
use crate::response_debug_context::extract_response_debug_context;
use crate::response_debug_context::telemetry_transport_error_message;
use crate::util::FeedbackRequestTags;
@@ -179,7 +178,7 @@ pub struct ModelsManager {
remote_models: RwLock<Vec<ModelInfo>>,
catalog_mode: CatalogMode,
collaboration_modes_config: CollaborationModesConfig,
auth_manager: Arc<AuthManager>,
request_auth: RequestAuthContext,
etag: RwLock<Option<String>>,
cache_manager: ModelsCacheManager,
provider: ModelProviderInfo,
@@ -231,7 +230,10 @@ impl ModelsManager {
remote_models: RwLock::new(remote_models),
catalog_mode,
collaboration_modes_config,
auth_manager,
request_auth: RequestAuthContext::new(
Some(auth_manager.clone()),
/*external_request_auth_source*/ None,
),
etag: RwLock::new(None),
cache_manager,
provider,
@@ -398,7 +400,13 @@ impl ModelsManager {
return Ok(());
}
if self.auth_manager.auth_mode() != Some(AuthMode::Chatgpt) {
if self
.request_auth
.auth_manager()
.and_then(|auth_manager| auth_manager.auth_mode())
!= Some(AuthMode::Chatgpt)
&& !self.provider.has_command_auth()
{
if matches!(
refresh_strategy,
RefreshStrategy::Offline | RefreshStrategy::OnlineIfUncached
@@ -435,16 +443,20 @@ impl ModelsManager {
codex_otel::start_global_timer("codex.remote_models.fetch_update.duration_ms", &[]);
let auth_env = collect_auth_env_telemetry(
&self.provider,
self.auth_manager.codex_api_key_env_enabled(),
self.request_auth
.auth_manager()
.is_some_and(|manager| manager.codex_api_key_env_enabled()),
);
let client_version = crate::models_manager::client_version_to_whole();
let mut unauthorized_recovery = RequestUnauthorizedRecovery::new(Some(&self.auth_manager));
let mut unauthorized_recovery = self.request_auth.unauthorized_recovery();
// Only loop after a successful auth-recovery step so `/models` retries with
// the same freshly resolved auth state as normal request paths.
loop {
let request_auth =
resolve_request_auth(Some(&self.auth_manager), &self.provider).await?;
let request_auth = self.request_auth.resolve(&self.provider).await?;
unauthorized_recovery.set_current_request_auth_access_token(
request_auth.request_auth_access_token.clone(),
);
let transport = ReqwestTransport::new(build_reqwest_client());
let request_telemetry: Arc<dyn RequestTelemetry> = Arc::new(ModelsRequestTelemetry {
auth_mode: request_auth
@@ -544,7 +556,12 @@ impl ModelsManager {
remote_models.sort_by(|a, b| a.priority.cmp(&b.priority));
let mut presets: Vec<ModelPreset> = remote_models.into_iter().map(Into::into).collect();
let chatgpt_mode = matches!(self.auth_manager.auth_mode(), Some(AuthMode::Chatgpt));
let chatgpt_mode = matches!(
self.request_auth
.auth_manager()
.and_then(|auth_manager| auth_manager.auth_mode()),
Some(AuthMode::Chatgpt)
);
presets = ModelPreset::filter_by_auth(presets, chatgpt_mode);
ModelPreset::mark_default_by_picker_visibility(&mut presets);
+176 -26
View File
@@ -1,41 +1,105 @@
use std::fmt;
use std::io;
use std::sync::Arc;
use crate::api_bridge::CoreAuthProvider;
use crate::api_bridge::auth_provider_from_auth;
use crate::api_bridge::auth_provider_from_resolved_provider_token;
use crate::auth::AuthManager;
use crate::auth::AuthMode;
use crate::auth::CodexAuth;
use crate::auth::ExternalAuthRefreshContext;
use crate::auth::ExternalAuthRefreshReason;
use crate::auth::ExternalAuthRefresher;
use crate::auth::RefreshTokenError;
use crate::auth::UnauthorizedRecovery;
use crate::error::CodexErr;
use crate::error::Result;
use crate::model_provider_info::ModelProviderInfo;
#[derive(Clone, Default)]
pub(crate) struct RequestAuthContext {
auth_manager: Option<Arc<AuthManager>>,
external_request_auth_source: Option<Arc<dyn ExternalAuthRefresher>>,
}
impl RequestAuthContext {
pub(crate) fn new(
auth_manager: Option<Arc<AuthManager>>,
external_request_auth_source: Option<Arc<dyn ExternalAuthRefresher>>,
) -> Self {
Self {
auth_manager,
external_request_auth_source,
}
}
pub(crate) async fn resolve(
&self,
provider: &ModelProviderInfo,
) -> Result<ResolvedRequestAuth> {
let auth = match self.auth_manager.as_ref() {
Some(manager) => manager.auth().await,
None => None,
};
let auth_mode = auth.as_ref().map(CodexAuth::auth_mode);
let request_auth_access_token = self.resolve_external_request_auth_access_token().await?;
let api_provider = provider.to_api_provider(auth_mode)?;
let api_auth = auth_provider_from_resolved_provider_token(
auth.clone(),
provider,
request_auth_access_token.clone(),
)?;
Ok(ResolvedRequestAuth {
auth,
auth_mode,
api_provider,
api_auth,
request_auth_access_token,
})
}
pub(crate) fn unauthorized_recovery(&self) -> RequestUnauthorizedRecovery {
RequestUnauthorizedRecovery::new(self)
}
pub(crate) fn auth_manager(&self) -> Option<&Arc<AuthManager>> {
self.auth_manager.as_ref()
}
pub(crate) async fn resolve_external_request_auth_access_token(
&self,
) -> Result<Option<String>> {
let Some(source) = self.external_request_auth_source.as_ref() else {
return Ok(None);
};
let tokens = source
.resolve()
.await
.map_err(map_external_request_auth_error)?;
Ok(tokens.map(|tokens| tokens.access_token))
}
}
impl fmt::Debug for RequestAuthContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RequestAuthContext")
.field("has_auth_manager", &self.auth_manager.is_some())
.field(
"has_external_request_auth_source",
&self.external_request_auth_source.is_some(),
)
.finish()
}
}
#[derive(Clone)]
pub(crate) struct ResolvedRequestAuth {
pub(crate) auth: Option<CodexAuth>,
pub(crate) auth_mode: Option<AuthMode>,
pub(crate) api_provider: codex_api::Provider,
pub(crate) api_auth: CoreAuthProvider,
}
pub(crate) async fn resolve_request_auth(
auth_manager: Option<&Arc<AuthManager>>,
provider: &ModelProviderInfo,
) -> Result<ResolvedRequestAuth> {
let auth = match auth_manager {
Some(manager) => manager.auth().await,
None => None,
};
let auth_mode = auth.as_ref().map(CodexAuth::auth_mode);
let api_provider = provider.to_api_provider(auth_mode)?;
let api_auth = auth_provider_from_auth(auth.clone(), provider)?;
Ok(ResolvedRequestAuth {
auth,
auth_mode,
api_provider,
api_auth,
})
pub(crate) request_auth_access_token: Option<String>,
}
#[derive(Clone, Copy, Debug)]
@@ -43,6 +107,7 @@ pub(crate) struct UnauthorizedRecoveryExecution {
pub(crate) mode: &'static str,
pub(crate) phase: &'static str,
pub(crate) auth_state_changed: Option<bool>,
pub(crate) refreshes_request_auth: bool,
}
#[derive(Clone, Copy, Debug)]
@@ -64,6 +129,10 @@ pub(crate) enum UnauthorizedRecoveryError {
execution: UnauthorizedRecoveryExecution,
error: RefreshTokenError,
},
RequestAuthSource {
execution: UnauthorizedRecoveryExecution,
error: CodexErr,
},
}
impl UnauthorizedRecoveryError {
@@ -73,6 +142,7 @@ impl UnauthorizedRecoveryError {
RefreshTokenError::Permanent(failed) => CodexErr::RefreshTokenFailed(failed),
RefreshTokenError::Transient(error) => CodexErr::Io(error),
},
Self::RequestAuthSource { error, .. } => error,
}
}
}
@@ -82,25 +152,75 @@ impl UnauthorizedRecoveryError {
/// The request loops reuse the same instance across retries so this type can enforce the
/// recovery ordering and make sure each one-shot step only runs once.
pub(crate) struct RequestUnauthorizedRecovery {
external_request_auth_source: Option<Arc<dyn ExternalAuthRefresher>>,
external_request_auth_retry_remaining: bool,
current_request_auth_access_token: Option<String>,
auth_recovery: Option<UnauthorizedRecovery>,
}
impl RequestUnauthorizedRecovery {
pub(crate) fn new(auth_manager: Option<&Arc<AuthManager>>) -> Self {
pub(crate) fn new(request_auth: &RequestAuthContext) -> Self {
Self {
auth_recovery: auth_manager.map(AuthManager::unauthorized_recovery),
external_request_auth_source: request_auth.external_request_auth_source.clone(),
external_request_auth_retry_remaining: request_auth
.external_request_auth_source
.is_some(),
current_request_auth_access_token: None,
auth_recovery: request_auth
.auth_manager
.as_ref()
.map(AuthManager::unauthorized_recovery),
}
}
pub(crate) fn set_current_request_auth_access_token(&mut self, access_token: Option<String>) {
self.current_request_auth_access_token = access_token;
}
/// Runs the next available auth-recovery step, if any.
///
/// Provider auth refresh is attempted first because it is specific to the configured
/// model provider and should repair the currently attached bearer token before falling
/// back to ChatGPT-managed recovery. Once all recovery steps have been consumed, this
/// returns `UnauthorizedRecoveryOutcome::Unavailable` so the caller can stop retrying.
/// Request-auth refresh is attempted first because it repairs the bearer token attached to the
/// failing request directly. If no request-auth source is configured for the request, or the
/// retry has already been consumed, recovery falls back to the ChatGPT auth-manager flow.
/// Once all recovery steps have been consumed, this returns
/// `UnauthorizedRecoveryOutcome::Unavailable` so the caller can stop retrying.
pub(crate) async fn next(
&mut self,
) -> std::result::Result<UnauthorizedRecoveryOutcome, UnauthorizedRecoveryError> {
if self.external_request_auth_retry_remaining
&& let Some(source) = self.external_request_auth_source.as_ref()
&& self.current_request_auth_access_token.is_some()
{
self.external_request_auth_retry_remaining = false;
let execution = UnauthorizedRecoveryExecution {
mode: "external_request_auth",
phase: "refresh",
auth_state_changed: None,
refreshes_request_auth: true,
};
return match source
.refresh_after_unauthorized(
ExternalAuthRefreshContext {
reason: ExternalAuthRefreshReason::Unauthorized,
previous_account_id: None,
},
self.current_request_auth_access_token.as_deref(),
)
.await
{
Ok(auth_state_changed) => Ok(UnauthorizedRecoveryOutcome::Recovered(
UnauthorizedRecoveryExecution {
auth_state_changed,
..execution
},
)),
Err(error) => Err(UnauthorizedRecoveryError::RequestAuthSource {
execution,
error: map_external_request_auth_error(error),
}),
};
}
if let Some(recovery) = self.auth_recovery.as_mut()
&& recovery.has_next()
{
@@ -108,6 +228,7 @@ impl RequestUnauthorizedRecovery {
mode: recovery.mode_name(),
phase: recovery.step_name(),
auth_state_changed: None,
refreshes_request_auth: false,
};
return match recovery.next().await {
Ok(step_result) => Ok(UnauthorizedRecoveryOutcome::Recovered(
@@ -120,6 +241,31 @@ impl RequestUnauthorizedRecovery {
};
}
if self.external_request_auth_source.is_some()
&& self.current_request_auth_access_token.is_none()
{
return Ok(UnauthorizedRecoveryOutcome::Unavailable(
UnauthorizedRecoveryUnavailable {
mode: "external_request_auth",
phase: "refresh",
reason: "request_auth_not_attached",
},
));
}
if self.external_request_auth_source.is_some()
&& !self.external_request_auth_retry_remaining
&& self.auth_recovery.is_none()
{
return Ok(UnauthorizedRecoveryOutcome::Unavailable(
UnauthorizedRecoveryUnavailable {
mode: "external_request_auth",
phase: "refresh",
reason: "request_auth_recovery_exhausted",
},
));
}
let unavailable = match self.auth_recovery.as_ref() {
Some(recovery) => UnauthorizedRecoveryUnavailable {
mode: recovery.mode_name(),
@@ -135,3 +281,7 @@ impl RequestUnauthorizedRecovery {
Ok(UnauthorizedRecoveryOutcome::Unavailable(unavailable))
}
}
fn map_external_request_auth_error(error: io::Error) -> CodexErr {
CodexErr::InvalidRequest(error.to_string())
}
+10
View File
@@ -205,6 +205,16 @@ fn unauthorized_recovery_reports_mode_and_step_names() {
assert_eq!(external.step_name(), "external_refresh");
}
#[test]
fn external_auth_tokens_without_chatgpt_metadata_cannot_seed_chatgpt_auth() {
let err = AuthDotJson::from_external_tokens(&ExternalAuthTokens::access_token_only("token"))
.expect_err("ChatGPT auth should require ChatGPT metadata");
assert_eq!(
err.to_string(),
"external auth tokens are missing ChatGPT metadata"
);
}
#[test]
fn refresh_failure_is_scoped_to_the_matching_auth_snapshot() {
let codex_home = tempdir().unwrap();
+68 -13
View File
@@ -90,11 +90,43 @@ pub enum RefreshTokenError {
Transient(#[from] std::io::Error),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ExternalAuthChatgptMetadata {
pub account_id: String,
pub plan_type: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ExternalAuthTokens {
pub access_token: String,
pub chatgpt_account_id: String,
pub chatgpt_plan_type: Option<String>,
pub chatgpt_metadata: Option<ExternalAuthChatgptMetadata>,
}
impl ExternalAuthTokens {
pub fn access_token_only(access_token: impl Into<String>) -> Self {
Self {
access_token: access_token.into(),
chatgpt_metadata: None,
}
}
pub fn chatgpt(
access_token: impl Into<String>,
chatgpt_account_id: impl Into<String>,
chatgpt_plan_type: Option<String>,
) -> Self {
Self {
access_token: access_token.into(),
chatgpt_metadata: Some(ExternalAuthChatgptMetadata {
account_id: chatgpt_account_id.into(),
plan_type: chatgpt_plan_type,
}),
}
}
pub fn chatgpt_metadata(&self) -> Option<&ExternalAuthChatgptMetadata> {
self.chatgpt_metadata.as_ref()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -110,10 +142,23 @@ pub struct ExternalAuthRefreshContext {
#[async_trait]
pub trait ExternalAuthRefresher: Send + Sync {
async fn resolve(&self) -> std::io::Result<Option<ExternalAuthTokens>> {
Ok(None)
}
async fn refresh(
&self,
context: ExternalAuthRefreshContext,
) -> std::io::Result<ExternalAuthTokens>;
async fn refresh_after_unauthorized(
&self,
context: ExternalAuthRefreshContext,
previous_access_token: Option<&str>,
) -> std::io::Result<Option<bool>> {
let refreshed = self.refresh(context).await?;
Ok(previous_access_token.map(|token| token != refreshed.access_token))
}
}
impl RefreshTokenError {
@@ -736,11 +781,16 @@ fn refresh_token_endpoint() -> String {
impl AuthDotJson {
fn from_external_tokens(external: &ExternalAuthTokens) -> std::io::Result<Self> {
let Some(chatgpt_metadata) = external.chatgpt_metadata() else {
return Err(std::io::Error::other(
"external auth tokens are missing ChatGPT metadata",
));
};
let mut token_info =
parse_chatgpt_jwt_claims(&external.access_token).map_err(std::io::Error::other)?;
token_info.chatgpt_account_id = Some(external.chatgpt_account_id.clone());
token_info.chatgpt_plan_type = external
.chatgpt_plan_type
token_info.chatgpt_account_id = Some(chatgpt_metadata.account_id.clone());
token_info.chatgpt_plan_type = chatgpt_metadata
.plan_type
.as_deref()
.map(InternalPlanType::from_raw_value)
.or(token_info.chatgpt_plan_type)
@@ -749,7 +799,7 @@ impl AuthDotJson {
id_token: token_info,
access_token: external.access_token.clone(),
refresh_token: String::new(),
account_id: Some(external.chatgpt_account_id.clone()),
account_id: Some(chatgpt_metadata.account_id.clone()),
};
Ok(Self {
@@ -765,11 +815,11 @@ impl AuthDotJson {
chatgpt_account_id: &str,
chatgpt_plan_type: Option<&str>,
) -> std::io::Result<Self> {
let external = ExternalAuthTokens {
access_token: access_token.to_string(),
chatgpt_account_id: chatgpt_account_id.to_string(),
chatgpt_plan_type: chatgpt_plan_type.map(str::to_string),
};
let external = ExternalAuthTokens::chatgpt(
access_token,
chatgpt_account_id,
chatgpt_plan_type.map(str::to_string),
);
Self::from_external_tokens(&external)
}
@@ -1457,13 +1507,18 @@ impl AuthManager {
};
let refreshed = refresher.refresh(context).await?;
let Some(chatgpt_metadata) = refreshed.chatgpt_metadata() else {
return Err(RefreshTokenError::Transient(std::io::Error::other(
"external auth refresh did not return ChatGPT metadata",
)));
};
if let Some(expected_workspace_id) = forced_chatgpt_workspace_id.as_deref()
&& refreshed.chatgpt_account_id != expected_workspace_id
&& chatgpt_metadata.account_id != expected_workspace_id
{
return Err(RefreshTokenError::Transient(std::io::Error::other(
format!(
"external auth refresh returned workspace {:?}, expected {expected_workspace_id:?}",
refreshed.chatgpt_account_id,
chatgpt_metadata.account_id,
),
)));
}
+5
View File
@@ -22,6 +22,11 @@ pub use auth::AuthManager;
pub use auth::CLIENT_ID;
pub use auth::CODEX_API_KEY_ENV_VAR;
pub use auth::CodexAuth;
pub use auth::ExternalAuthChatgptMetadata;
pub use auth::ExternalAuthRefreshContext;
pub use auth::ExternalAuthRefreshReason;
pub use auth::ExternalAuthRefresher;
pub use auth::ExternalAuthTokens;
pub use auth::OPENAI_API_KEY_ENV_VAR;
pub use auth::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR;
pub use auth::RefreshTokenError;