chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
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) async throws -> CodexOpenAIWorkspaceIdentity? {
|
||||
try await self.resolve(credentials: credentials, session: CodexAuthenticatedHTTPTransport.current)
|
||||
}
|
||||
|
||||
public static func resolve(
|
||||
credentials: CodexOAuthCredentials,
|
||||
session transport: any ProviderHTTPTransport) async throws -> CodexOpenAIWorkspaceIdentity?
|
||||
{
|
||||
guard let workspaceAccountID = normalizeWorkspaceAccountID(credentials.accountId) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let identities = try await self.listWorkspaces(credentials: credentials, session: transport)
|
||||
if let identity = identities.first(where: { $0.workspaceAccountID == workspaceAccountID }) {
|
||||
return identity
|
||||
}
|
||||
|
||||
return CodexOpenAIWorkspaceIdentity(
|
||||
workspaceAccountID: workspaceAccountID,
|
||||
workspaceLabel: nil)
|
||||
}
|
||||
|
||||
public static func listWorkspaces(
|
||||
credentials: CodexOAuthCredentials) async throws -> [CodexOpenAIWorkspaceIdentity]
|
||||
{
|
||||
try await self.listWorkspaces(credentials: credentials, session: CodexAuthenticatedHTTPTransport.current)
|
||||
}
|
||||
|
||||
public static func listWorkspaces(
|
||||
credentials: CodexOAuthCredentials,
|
||||
session transport: any ProviderHTTPTransport) async throws -> [CodexOpenAIWorkspaceIdentity]
|
||||
{
|
||||
var request = URLRequest(
|
||||
url: self.accountsURL,
|
||||
cachePolicy: .reloadIgnoringLocalCacheData,
|
||||
timeoutInterval: 20)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("Bearer \(credentials.accessToken)", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("codex-cli", forHTTPHeaderField: "User-Agent")
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
if let workspaceAccountID = normalizeWorkspaceAccountID(credentials.accountId) {
|
||||
request.setValue(workspaceAccountID, forHTTPHeaderField: "ChatGPT-Account-Id")
|
||||
}
|
||||
|
||||
let response = try await transport.response(for: request)
|
||||
guard (200..<300).contains(response.statusCode)
|
||||
else {
|
||||
throw CodexOpenAIWorkspaceResolverError.invalidResponse
|
||||
}
|
||||
|
||||
let decoded = try JSONDecoder().decode(AccountsResponse.self, from: response.data)
|
||||
return decoded.items.compactMap { account in
|
||||
guard let workspaceAccountID = self.normalizeWorkspaceAccountID(account.id) else { return nil }
|
||||
return CodexOpenAIWorkspaceIdentity(
|
||||
workspaceAccountID: workspaceAccountID,
|
||||
workspaceLabel: self.resolveWorkspaceLabel(from: account))
|
||||
}
|
||||
}
|
||||
|
||||
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."
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user