chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import Foundation
|
||||
|
||||
public enum FileManagedCodexAccountStoreError: Error, Equatable, Sendable {
|
||||
case unsupportedVersion(Int)
|
||||
}
|
||||
|
||||
public protocol ManagedCodexAccountStoring: Sendable {
|
||||
func loadAccounts() throws -> ManagedCodexAccountSet
|
||||
func storeAccounts(_ accounts: ManagedCodexAccountSet) throws
|
||||
func ensureFileExists() throws -> URL
|
||||
}
|
||||
|
||||
public struct FileManagedCodexAccountStore: ManagedCodexAccountStoring, @unchecked Sendable {
|
||||
public static let currentVersion = 3
|
||||
|
||||
private let fileURL: URL
|
||||
private let fileManager: FileManager
|
||||
|
||||
public init(fileURL: URL = Self.defaultURL(), fileManager: FileManager = .default) {
|
||||
self.fileURL = fileURL
|
||||
self.fileManager = fileManager
|
||||
}
|
||||
|
||||
public func loadAccounts() throws -> ManagedCodexAccountSet {
|
||||
guard self.fileManager.fileExists(atPath: self.fileURL.path) else {
|
||||
return Self.emptyAccountSet()
|
||||
}
|
||||
|
||||
let data = try Data(contentsOf: self.fileURL)
|
||||
let decoder = JSONDecoder()
|
||||
let accounts = try decoder.decode(ManagedCodexAccountSet.self, from: data)
|
||||
guard (1...Self.currentVersion).contains(accounts.version) else {
|
||||
throw FileManagedCodexAccountStoreError.unsupportedVersion(accounts.version)
|
||||
}
|
||||
if accounts.version == Self.currentVersion {
|
||||
return ManagedCodexAccountSet(version: Self.currentVersion, accounts: accounts.accounts)
|
||||
}
|
||||
return self.migrateLegacyAccounts(accounts)
|
||||
}
|
||||
|
||||
public func storeAccounts(_ accounts: ManagedCodexAccountSet) throws {
|
||||
let normalizedAccounts = ManagedCodexAccountSet(
|
||||
version: Self.currentVersion,
|
||||
accounts: accounts.accounts)
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
||||
let data = try encoder.encode(normalizedAccounts)
|
||||
let directory = self.fileURL.deletingLastPathComponent()
|
||||
if !self.fileManager.fileExists(atPath: directory.path) {
|
||||
try self.fileManager.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||
}
|
||||
try data.write(to: self.fileURL, options: [.atomic])
|
||||
try self.applySecurePermissionsIfNeeded()
|
||||
}
|
||||
|
||||
public func ensureFileExists() throws -> URL {
|
||||
if self.fileManager.fileExists(atPath: self.fileURL.path) { return self.fileURL }
|
||||
try self.storeAccounts(Self.emptyAccountSet())
|
||||
return self.fileURL
|
||||
}
|
||||
|
||||
private func applySecurePermissionsIfNeeded() throws {
|
||||
#if os(macOS)
|
||||
try self.fileManager.setAttributes([
|
||||
.posixPermissions: NSNumber(value: Int16(0o600)),
|
||||
], ofItemAtPath: self.fileURL.path)
|
||||
#endif
|
||||
}
|
||||
|
||||
private static func emptyAccountSet() -> ManagedCodexAccountSet {
|
||||
ManagedCodexAccountSet(version: self.currentVersion, accounts: [])
|
||||
}
|
||||
|
||||
private func migrateLegacyAccounts(_ accounts: ManagedCodexAccountSet) -> ManagedCodexAccountSet {
|
||||
let migratedAccounts = accounts.accounts.map { account in
|
||||
let hydratedProviderAccountID = account.providerAccountID ?? self.hydrateProviderAccountID(for: account)
|
||||
return ManagedCodexAccount(
|
||||
id: account.id,
|
||||
email: account.email,
|
||||
providerAccountID: hydratedProviderAccountID,
|
||||
workspaceLabel: account.workspaceLabel,
|
||||
workspaceAccountID: account.workspaceAccountID,
|
||||
authFingerprint: account.authFingerprint ?? CodexAuthFingerprint.fingerprint(
|
||||
homePath: account.managedHomePath,
|
||||
fileManager: self.fileManager),
|
||||
managedHomePath: account.managedHomePath,
|
||||
createdAt: account.createdAt,
|
||||
updatedAt: account.updatedAt,
|
||||
lastAuthenticatedAt: account.lastAuthenticatedAt)
|
||||
}
|
||||
return ManagedCodexAccountSet(version: Self.currentVersion, accounts: migratedAccounts)
|
||||
}
|
||||
|
||||
private func hydrateProviderAccountID(for account: ManagedCodexAccount) -> String? {
|
||||
guard let credentials = try? CodexOAuthCredentialsStore.load(
|
||||
env: ["CODEX_HOME": account.managedHomePath])
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
let payload = credentials.idToken.flatMap(UsageFetcher.parseJWT)
|
||||
let authDict = payload?["https://api.openai.com/auth"] as? [String: Any]
|
||||
let providerAccountID = credentials.accountId
|
||||
?? (authDict?["chatgpt_account_id"] as? String)
|
||||
?? (payload?["chatgpt_account_id"] as? String)
|
||||
return ManagedCodexAccount.normalizeProviderAccountID(providerAccountID)
|
||||
}
|
||||
|
||||
public static func defaultURL() -> URL {
|
||||
let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
|
||||
?? FileManager.default.homeDirectoryForCurrentUser
|
||||
return base
|
||||
.appendingPathComponent("CodexBar", isDirectory: true)
|
||||
.appendingPathComponent("managed-codex-accounts.json")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user