chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
import CodexBarCore
|
||||
import Darwin
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
protocol CodexAccountReconciliationSnapshotLoading {
|
||||
func loadSnapshot() -> CodexAccountReconciliationSnapshot
|
||||
}
|
||||
|
||||
protocol CodexAuthMaterialReading: Sendable {
|
||||
func readAuthData(homeURL: URL) throws -> Data?
|
||||
}
|
||||
|
||||
protocol CodexLiveAuthSwapping: Sendable {
|
||||
func swapLiveAuthData(_ data: Data, liveHomeURL: URL) throws
|
||||
}
|
||||
|
||||
@MainActor
|
||||
protocol CodexActiveSourceWriting {
|
||||
func writeCodexActiveSource(_ source: CodexActiveSource)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
protocol CodexAccountScopedRefreshing {
|
||||
func refreshCodexAccountScopedState(allowDisabled: Bool) async
|
||||
}
|
||||
|
||||
@MainActor
|
||||
struct SettingsStoreCodexAccountReconciliationSnapshotLoader: CodexAccountReconciliationSnapshotLoading {
|
||||
private let settingsStore: SettingsStore
|
||||
|
||||
init(settingsStore: SettingsStore) {
|
||||
self.settingsStore = settingsStore
|
||||
}
|
||||
|
||||
func loadSnapshot() -> CodexAccountReconciliationSnapshot {
|
||||
self.settingsStore.codexAccountReconciliationSnapshot
|
||||
}
|
||||
}
|
||||
|
||||
struct DefaultCodexAuthMaterialReader: CodexAuthMaterialReading {
|
||||
func readAuthData(homeURL: URL) throws -> Data? {
|
||||
let authFileURL = CodexAccountPromotionService.authFileURL(for: homeURL)
|
||||
guard FileManager.default.fileExists(atPath: authFileURL.path) else {
|
||||
return nil
|
||||
}
|
||||
return try Data(contentsOf: authFileURL)
|
||||
}
|
||||
}
|
||||
|
||||
struct DefaultCodexLiveAuthSwapper: CodexLiveAuthSwapping {
|
||||
func swapLiveAuthData(_ data: Data, liveHomeURL: URL) throws {
|
||||
try FileManager.default.createDirectory(at: liveHomeURL, withIntermediateDirectories: true)
|
||||
|
||||
let liveAuthURL = CodexAccountPromotionService.authFileURL(for: liveHomeURL)
|
||||
let stagedAuthURL = liveHomeURL.appendingPathComponent(
|
||||
"auth.json.codexbar-staged-\(UUID().uuidString)",
|
||||
isDirectory: false)
|
||||
|
||||
do {
|
||||
try data.write(to: stagedAuthURL)
|
||||
try FileManager.default.setAttributes(
|
||||
[.posixPermissions: NSNumber(value: Int16(0o600))],
|
||||
ofItemAtPath: stagedAuthURL.path)
|
||||
try self.renameItem(at: stagedAuthURL, to: liveAuthURL)
|
||||
} catch {
|
||||
try? FileManager.default.removeItem(at: stagedAuthURL)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private func renameItem(at sourceURL: URL, to destinationURL: URL) throws {
|
||||
let sourcePath = sourceURL.path
|
||||
let destinationPath = destinationURL.path
|
||||
|
||||
let result = sourcePath.withCString { sourceFS in
|
||||
destinationPath.withCString { destinationFS in
|
||||
rename(sourceFS, destinationFS)
|
||||
}
|
||||
}
|
||||
|
||||
guard result == 0 else {
|
||||
throw NSError(
|
||||
domain: NSPOSIXErrorDomain,
|
||||
code: Int(errno),
|
||||
userInfo: [NSFilePathErrorKey: destinationPath])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
struct SettingsStoreCodexActiveSourceWriter: CodexActiveSourceWriting {
|
||||
private let settingsStore: SettingsStore
|
||||
|
||||
init(settingsStore: SettingsStore) {
|
||||
self.settingsStore = settingsStore
|
||||
}
|
||||
|
||||
func writeCodexActiveSource(_ source: CodexActiveSource) {
|
||||
self.settingsStore.codexActiveSource = source
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
struct UsageStoreCodexAccountScopedRefresher: CodexAccountScopedRefreshing {
|
||||
private let usageStore: UsageStore
|
||||
|
||||
init(usageStore: UsageStore) {
|
||||
self.usageStore = usageStore
|
||||
}
|
||||
|
||||
func refreshCodexAccountScopedState(allowDisabled: Bool) async {
|
||||
await self.usageStore.refreshCodexAccountScopedState(allowDisabled: allowDisabled)
|
||||
}
|
||||
}
|
||||
|
||||
struct CodexAccountPromotionResult: Equatable {
|
||||
enum Outcome: Equatable {
|
||||
case promoted
|
||||
case convergedNoOp
|
||||
}
|
||||
|
||||
enum DisplacedLiveDisposition: Equatable {
|
||||
case none
|
||||
case alreadyManaged(managedAccountID: UUID)
|
||||
case imported(managedAccountID: UUID)
|
||||
}
|
||||
|
||||
let targetManagedAccountID: UUID
|
||||
let outcome: Outcome
|
||||
let displacedLiveDisposition: DisplacedLiveDisposition
|
||||
let didMutateLiveAuth: Bool
|
||||
let resultingActiveSource: CodexActiveSource
|
||||
}
|
||||
|
||||
enum CodexAccountPromotionError: Error, Equatable {
|
||||
case targetManagedAccountNotFound
|
||||
case targetManagedAccountAuthMissing
|
||||
case targetManagedAccountAuthUnreadable
|
||||
case liveAccountUnreadable
|
||||
case liveAccountMissingIdentityForPreservation
|
||||
case liveAccountAPIKeyOnlyUnsupported
|
||||
case displacedLiveManagedAccountConflict
|
||||
case displacedLiveImportFailed
|
||||
case managedStoreCommitFailed
|
||||
case liveAuthSwapFailed
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class CodexAccountPromotionService {
|
||||
private let store: any ManagedCodexAccountStoring
|
||||
private let homeFactory: any ManagedCodexHomeProducing
|
||||
private let identityReader: any ManagedCodexIdentityReading
|
||||
private let workspaceResolver: any ManagedCodexWorkspaceResolving
|
||||
private let snapshotLoader: any CodexAccountReconciliationSnapshotLoading
|
||||
private let authMaterialReader: any CodexAuthMaterialReading
|
||||
private let liveAuthSwapper: any CodexLiveAuthSwapping
|
||||
private let activeSourceWriter: any CodexActiveSourceWriting
|
||||
private let accountScopedRefresher: any CodexAccountScopedRefreshing
|
||||
private let baseEnvironment: [String: String]
|
||||
private let fileManager: FileManager
|
||||
|
||||
init(
|
||||
store: any ManagedCodexAccountStoring,
|
||||
homeFactory: any ManagedCodexHomeProducing,
|
||||
identityReader: any ManagedCodexIdentityReading,
|
||||
workspaceResolver: any ManagedCodexWorkspaceResolving = DefaultManagedCodexWorkspaceResolver(),
|
||||
snapshotLoader: any CodexAccountReconciliationSnapshotLoading,
|
||||
authMaterialReader: any CodexAuthMaterialReading,
|
||||
liveAuthSwapper: any CodexLiveAuthSwapping,
|
||||
activeSourceWriter: any CodexActiveSourceWriting,
|
||||
accountScopedRefresher: any CodexAccountScopedRefreshing,
|
||||
baseEnvironment: [String: String] = ProcessInfo.processInfo.environment,
|
||||
fileManager: FileManager = .default)
|
||||
{
|
||||
self.store = store
|
||||
self.homeFactory = homeFactory
|
||||
self.identityReader = identityReader
|
||||
self.workspaceResolver = workspaceResolver
|
||||
self.snapshotLoader = snapshotLoader
|
||||
self.authMaterialReader = authMaterialReader
|
||||
self.liveAuthSwapper = liveAuthSwapper
|
||||
self.activeSourceWriter = activeSourceWriter
|
||||
self.accountScopedRefresher = accountScopedRefresher
|
||||
self.baseEnvironment = baseEnvironment
|
||||
self.fileManager = fileManager
|
||||
}
|
||||
|
||||
convenience init(
|
||||
settingsStore: SettingsStore,
|
||||
usageStore: UsageStore,
|
||||
baseEnvironment: [String: String] = ProcessInfo.processInfo.environment,
|
||||
fileManager: FileManager = .default)
|
||||
{
|
||||
self.init(
|
||||
store: FileManagedCodexAccountStore(fileManager: fileManager),
|
||||
homeFactory: ManagedCodexHomeFactory(fileManager: fileManager),
|
||||
identityReader: DefaultManagedCodexIdentityReader(),
|
||||
workspaceResolver: DefaultManagedCodexWorkspaceResolver(),
|
||||
snapshotLoader: SettingsStoreCodexAccountReconciliationSnapshotLoader(settingsStore: settingsStore),
|
||||
authMaterialReader: DefaultCodexAuthMaterialReader(),
|
||||
liveAuthSwapper: DefaultCodexLiveAuthSwapper(),
|
||||
activeSourceWriter: SettingsStoreCodexActiveSourceWriter(settingsStore: settingsStore),
|
||||
accountScopedRefresher: UsageStoreCodexAccountScopedRefresher(usageStore: usageStore),
|
||||
baseEnvironment: baseEnvironment,
|
||||
fileManager: fileManager)
|
||||
}
|
||||
|
||||
func promoteManagedAccount(id: UUID) async throws -> CodexAccountPromotionResult {
|
||||
let contextBuilder = PreparedPromotionContextBuilder(
|
||||
store: self.store,
|
||||
workspaceResolver: self.workspaceResolver,
|
||||
snapshotLoader: self.snapshotLoader,
|
||||
authMaterialReader: self.authMaterialReader,
|
||||
baseEnvironment: self.baseEnvironment,
|
||||
fileManager: self.fileManager)
|
||||
let context = try await contextBuilder.build(targetID: id)
|
||||
|
||||
if let resultingActiveSource = self.convergedActiveSource(for: context) {
|
||||
self.activeSourceWriter.writeCodexActiveSource(resultingActiveSource)
|
||||
await self.accountScopedRefresher.refreshCodexAccountScopedState(allowDisabled: true)
|
||||
return CodexAccountPromotionResult(
|
||||
targetManagedAccountID: id,
|
||||
outcome: .convergedNoOp,
|
||||
displacedLiveDisposition: .none,
|
||||
didMutateLiveAuth: false,
|
||||
resultingActiveSource: resultingActiveSource)
|
||||
}
|
||||
|
||||
let targetAuthMaterial = try self.requiredTargetAuthMaterial(from: context.target)
|
||||
let preservationPlan = CodexDisplacedLivePreservationPlanner().makePlan(context: context)
|
||||
let executionResult = try CodexDisplacedLivePreservationExecutor(
|
||||
store: self.store,
|
||||
homeFactory: self.homeFactory,
|
||||
fileManager: self.fileManager)
|
||||
.execute(plan: preservationPlan, context: context)
|
||||
|
||||
do {
|
||||
try self.liveAuthSwapper.swapLiveAuthData(targetAuthMaterial.rawData, liveHomeURL: context.live.homeURL)
|
||||
} catch {
|
||||
throw CodexAccountPromotionError.liveAuthSwapFailed
|
||||
}
|
||||
|
||||
self.activeSourceWriter.writeCodexActiveSource(.liveSystem)
|
||||
await self.accountScopedRefresher.refreshCodexAccountScopedState(allowDisabled: true)
|
||||
|
||||
return CodexAccountPromotionResult(
|
||||
targetManagedAccountID: id,
|
||||
outcome: .promoted,
|
||||
displacedLiveDisposition: executionResult.displacedLiveDisposition,
|
||||
didMutateLiveAuth: true,
|
||||
resultingActiveSource: .liveSystem)
|
||||
}
|
||||
|
||||
nonisolated static func authFileURL(for homeURL: URL) -> URL {
|
||||
homeURL.appendingPathComponent("auth.json", isDirectory: false)
|
||||
}
|
||||
|
||||
private func convergedActiveSource(for context: PreparedPromotionContext) -> CodexActiveSource? {
|
||||
if let liveAuthIdentity = context.live.authIdentity {
|
||||
let targetIdentity = context.target.authIdentity ?? context.target.persistedIdentity
|
||||
guard CodexIdentityMatcher.matches(
|
||||
targetIdentity.identity,
|
||||
lhsEmail: targetIdentity.email,
|
||||
liveAuthIdentity.identity,
|
||||
rhsEmail: liveAuthIdentity.email)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if liveAuthIdentity.email != nil {
|
||||
return .liveSystem
|
||||
}
|
||||
|
||||
if liveAuthIdentity.providerAccountID != nil {
|
||||
return .managedAccount(id: context.target.persisted.id)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let liveSystemAccount = context.snapshot.liveSystemAccount else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard CodexIdentityMatcher.matches(
|
||||
context.snapshot.runtimeIdentity(for: context.target.persisted),
|
||||
lhsEmail: context.snapshot.runtimeEmail(for: context.target.persisted),
|
||||
context.snapshot.runtimeIdentity(for: liveSystemAccount),
|
||||
rhsEmail: liveSystemAccount.email)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return .liveSystem
|
||||
}
|
||||
|
||||
private func requiredTargetAuthMaterial(from target: PreparedStoredManagedAccount) throws -> PreparedAuthMaterial {
|
||||
switch target.homeState {
|
||||
case let .readable(authMaterial):
|
||||
guard authMaterial.authIdentity.email != nil else {
|
||||
throw CodexAccountPromotionError.targetManagedAccountAuthUnreadable
|
||||
}
|
||||
return authMaterial
|
||||
case .missing:
|
||||
throw CodexAccountPromotionError.targetManagedAccountAuthMissing
|
||||
case .unreadable:
|
||||
throw CodexAccountPromotionError.targetManagedAccountAuthUnreadable
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user