diff --git a/codex-rs/app-server-protocol/src/protocol/v2/account.rs b/codex-rs/app-server-protocol/src/protocol/v2/account.rs index c2a538982..83f1bc02b 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/account.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/account.rs @@ -138,6 +138,78 @@ pub struct CancelLoginAccountResponse { pub status: CancelLoginAccountStatus, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct AccountSessionsAddParams { + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub switch_to_added_account: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct AccountSessionsListParams { + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub refresh_workspace_metadata: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct AccountSessionsLogoutParams { + pub session_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct AccountSessionsSwitchParams { + pub session_id: String, + pub account_id: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct AccountSessionsResponse { + pub active_session_id: Option, + pub sessions: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct AccountSession { + pub session_id: String, + pub email: Option, + pub user_id: Option, + pub display_name: Option, + pub image_url: Option, + pub last_used_at: i64, + pub is_active: bool, + pub selected_workspace_account_id: Option, + pub workspaces: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct AccountSessionWorkspace { + pub account_id: String, + pub name: Option, + pub image_url: Option, + pub kind: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub enum AccountSessionWorkspaceKind { + Personal, + Workspace, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] diff --git a/codex-rs/backend-client/src/client.rs b/codex-rs/backend-client/src/client.rs index d9bdc9654..24d277659 100644 --- a/codex-rs/backend-client/src/client.rs +++ b/codex-rs/backend-client/src/client.rs @@ -1,3 +1,4 @@ +use crate::types::AccountsCheckResponse; use crate::types::CodeTaskDetailsResponse; use crate::types::ConfigBundleResponse; use crate::types::PaginatedListTaskListItem; @@ -302,6 +303,16 @@ impl Client { Ok(Self::rate_limit_snapshots_from_payload(payload)) } + pub async fn get_accounts_check(&self) -> Result { + let url = match self.path_style { + PathStyle::CodexApi => format!("{}/api/codex/accounts/check", self.base_url), + PathStyle::ChatGptApi => format!("{}/wham/accounts/check", self.base_url), + }; + let req = self.http.get(&url).headers(self.headers()); + let (body, ct) = self.exec_request(req, "GET", &url).await?; + self.decode_json(&url, &ct, &body) + } + pub async fn send_add_credits_nudge_email( &self, credit_type: AddCreditsNudgeCreditType, diff --git a/codex-rs/backend-client/src/lib.rs b/codex-rs/backend-client/src/lib.rs index 250aac3d7..dfd7e816f 100644 --- a/codex-rs/backend-client/src/lib.rs +++ b/codex-rs/backend-client/src/lib.rs @@ -4,6 +4,8 @@ pub(crate) mod types; pub use client::AddCreditsNudgeCreditType; pub use client::Client; pub use client::RequestError; +pub use types::AccountEntry; +pub use types::AccountsCheckResponse; pub use types::CodeTaskDetailsResponse; pub use types::CodeTaskDetailsResponseExt; pub use types::ConfigBundleResponse; diff --git a/codex-rs/backend-client/src/types.rs b/codex-rs/backend-client/src/types.rs index 40daba2ce..06989241f 100644 --- a/codex-rs/backend-client/src/types.rs +++ b/codex-rs/backend-client/src/types.rs @@ -17,6 +17,93 @@ use serde::de::Deserializer; use serde_json::Value; use std::collections::HashMap; +#[derive(Clone, Debug)] +pub struct AccountsCheckResponse { + pub accounts: Vec, + pub account_ordering: Vec, + pub default_account_id: Option, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct AccountEntry { + pub id: String, + #[serde(default)] + pub name: Option, + #[serde(default)] + pub profile_picture_url: Option, + #[serde(default)] + pub structure: String, +} + +#[derive(Deserialize)] +struct RawAccountsCheckResponse { + #[serde(default)] + accounts: RawAccounts, + #[serde(default)] + account_ordering: Vec, + #[serde(default)] + default_account_id: Option, +} + +#[derive(Deserialize)] +#[serde(untagged)] +enum RawAccounts { + List(Vec), + Map(HashMap), +} + +impl Default for RawAccounts { + fn default() -> Self { + Self::List(Vec::new()) + } +} + +#[derive(Deserialize)] +struct ChatGptAccountEntry { + account: ChatGptAccountInfo, +} + +#[derive(Deserialize)] +struct ChatGptAccountInfo { + account_id: Option, + #[serde(default)] + name: Option, + #[serde(default)] + profile_picture_url: Option, + #[serde(default)] + structure: String, +} + +impl<'de> Deserialize<'de> for AccountsCheckResponse { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let raw = RawAccountsCheckResponse::deserialize(deserializer)?; + let accounts = match raw.accounts { + RawAccounts::List(accounts) => accounts, + RawAccounts::Map(mut accounts) => raw + .account_ordering + .iter() + .filter_map(|account_id| { + let account = accounts.remove(account_id)?.account; + Some(AccountEntry { + id: account.account_id?, + name: account.name, + profile_picture_url: account.profile_picture_url, + structure: account.structure, + }) + }) + .collect(), + }; + Ok(Self { + accounts, + account_ordering: raw.account_ordering, + default_account_id: raw.default_account_id, + }) + } +} + /// Hand-rolled models for the Cloud Tasks task-details response. /// The generated OpenAPI models are pretty bad. This is a half-step /// towards hand-rolling them.