101 lines · 3.5 KB · Swift
RawDownload
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."        }    }}