import Foundation#if canImport(FoundationNetworking)import FoundationNetworking#endif public struct CodexOpenAIWorkspaceIdentity: Equatable, Sendable { public let workspaceAccountID: String public let workspaceLabel: String? public init(workspaceAccountID: String, workspaceLabel: String?) { self.workspaceAccountID = Self.normalizeWorkspaceAccountID(workspaceAccountID) self.workspaceLabel = Self.normalizeWorkspaceLabel(workspaceLabel) } public static func normalizeWorkspaceAccountID(_ value: String) -> String { value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() } public static func normalizeWorkspaceLabel(_ value: String?) -> String? { guard let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines), !trimmed.isEmpty else { return nil } return trimmed }} public enum CodexOpenAIWorkspaceResolver { private struct AccountsResponse: Decodable { let items: [AccountItem] } private struct AccountItem: Decodable { let id: String let name: String? } private static let accountsURL = URL(string: "https://chatgpt.com/backend-api/accounts")! public static func resolve( credentials: CodexOAuthCredentials, session: URLSession = .shared) async throws -> CodexOpenAIWorkspaceIdentity? { guard let workspaceAccountID = normalizeWorkspaceAccountID(credentials.accountId) else { return nil } var request = URLRequest(url: self.accountsURL) request.httpMethod = "GET" request.timeoutInterval = 20 request.setValue("Bearer \(credentials.accessToken)", forHTTPHeaderField: "Authorization") request.setValue("codex-cli", forHTTPHeaderField: "User-Agent") request.setValue("application/json", forHTTPHeaderField: "Accept") request.setValue(workspaceAccountID, forHTTPHeaderField: "ChatGPT-Account-Id") let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse, (200..<300).contains(httpResponse.statusCode) else { throw CodexOpenAIWorkspaceResolverError.invalidResponse } let decoded = try JSONDecoder().decode(AccountsResponse.self, from: data) if let account = decoded.items.first(where: { Self.normalizeWorkspaceAccountID($0.id) == workspaceAccountID }) { return CodexOpenAIWorkspaceIdentity( workspaceAccountID: workspaceAccountID, workspaceLabel: self.resolveWorkspaceLabel(from: account)) } return CodexOpenAIWorkspaceIdentity( workspaceAccountID: workspaceAccountID, workspaceLabel: nil) } public static func normalizeWorkspaceAccountID(_ value: String?) -> String? { guard let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines), !trimmed.isEmpty else { return nil } return trimmed.lowercased() } private static func resolveWorkspaceLabel(from account: AccountItem) -> String? { let name = account.name?.trimmingCharacters(in: .whitespacesAndNewlines) if let name, !name.isEmpty { return name } return "Personal" }} public enum CodexOpenAIWorkspaceResolverError: LocalizedError, Sendable { case invalidResponse public var errorDescription: String? { switch self { case .invalidResponse: "OpenAI account lookup returned an invalid response." } }}
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
public struct CodexOpenAIWorkspaceIdentity: Equatable, Sendable {
public let workspaceAccountID: String
public let workspaceLabel: String?
public init(workspaceAccountID: String, workspaceLabel: String?) {
self.workspaceAccountID = Self.normalizeWorkspaceAccountID(workspaceAccountID)
self.workspaceLabel = Self.normalizeWorkspaceLabel(workspaceLabel)
}
public static func normalizeWorkspaceAccountID(_ value: String) -> String {
value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
}
public static func normalizeWorkspaceLabel(_ value: String?) -> String? {
guard let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines), !trimmed.isEmpty else {
return nil
}
return trimmed
}
}
public enum CodexOpenAIWorkspaceResolver {
private struct AccountsResponse: Decodable {
let items: [AccountItem]
}
private struct AccountItem: Decodable {
let id: String
let name: String?
}
private static let accountsURL = URL(string: "https://chatgpt.com/backend-api/accounts")!
public static func resolve(
credentials: CodexOAuthCredentials,
session: URLSession = .shared) async throws -> CodexOpenAIWorkspaceIdentity?
{
guard let workspaceAccountID = normalizeWorkspaceAccountID(credentials.accountId) else {
return nil
}
var request = URLRequest(url: self.accountsURL)
request.httpMethod = "GET"
request.timeoutInterval = 20
request.setValue("Bearer \(credentials.accessToken)", forHTTPHeaderField: "Authorization")
request.setValue("codex-cli", forHTTPHeaderField: "User-Agent")
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue(workspaceAccountID, forHTTPHeaderField: "ChatGPT-Account-Id")
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
(200..<300).contains(httpResponse.statusCode)
else {
throw CodexOpenAIWorkspaceResolverError.invalidResponse
}
let decoded = try JSONDecoder().decode(AccountsResponse.self, from: data)
if let account = decoded.items.first(where: {
Self.normalizeWorkspaceAccountID($0.id) == workspaceAccountID
}) {
return CodexOpenAIWorkspaceIdentity(
workspaceAccountID: workspaceAccountID,
workspaceLabel: self.resolveWorkspaceLabel(from: account))
}
return CodexOpenAIWorkspaceIdentity(
workspaceAccountID: workspaceAccountID,
workspaceLabel: nil)
}
public static func normalizeWorkspaceAccountID(_ value: String?) -> String? {
guard let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines), !trimmed.isEmpty else {
return nil
}
return trimmed.lowercased()
}
private static func resolveWorkspaceLabel(from account: AccountItem) -> String? {
let name = account.name?.trimmingCharacters(in: .whitespacesAndNewlines)
if let name, !name.isEmpty {
return name
}
return "Personal"
}
}
public enum CodexOpenAIWorkspaceResolverError: LocalizedError, Sendable {
case invalidResponse
public var errorDescription: String? {
switch self {
case .invalidResponse:
"OpenAI account lookup returned an invalid response."
}
}
}