diff --git a/Package.swift b/Package.swift index e1a050ba..3c0d7019 100644 --- a/Package.swift +++ b/Package.swift @@ -120,7 +120,7 @@ let package = Package( "DeveloperAPI", "CXKit", "XUtils", - .product(name: "XADI", package: "xadi", condition: .when(platforms: [.linux])), + .product(name: "XADI", package: "xadi", condition: .when(platforms: [.linux, .macOS])), .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), .product(name: "Dependencies", package: "swift-dependencies"), .product(name: "SwiftyMobileDevice", package: "SwiftyMobileDevice"), diff --git a/Sources/XKit/GrandSlam/Anisette/ADIDataProvider.swift b/Sources/XKit/GrandSlam/Anisette/ADIDataProvider.swift index 085730c4..490bae7c 100644 --- a/Sources/XKit/GrandSlam/Anisette/ADIDataProvider.swift +++ b/Sources/XKit/GrandSlam/Anisette/ADIDataProvider.swift @@ -245,6 +245,14 @@ public struct ADIDataProvider: AnisetteDataProvider { try? storage.setString(nil, forKey: Self.routingInfoKey) } + public var providerVersion: String? { + #if os(macOS) + return "1" + #else + return nil + #endif + } + public func provisioningData() -> ProvisioningData? { guard let provisioningInfo = try? storage.data(forKey: Self.provisioningKey), let routingInfoString = try? storage.string(forKey: Self.routingInfoKey), @@ -280,6 +288,23 @@ public struct ADIDataProvider: AnisetteDataProvider { } -public struct ADIError: Error { +public struct ADIError: Error, CustomStringConvertible { public var code: Int + + public var description: String { + switch code { + case -45061: + return """ + You were logged out. Please log in again with `xtool auth`. + + If you see this repeatedly, please file an issue at https://github.com/xtool-org/xtool/issues/new/choose + """ + default: + return """ + Apple Anisette library returned error \(code). Please try logging in again with `xtool auth`. + + If this problem persists, file an issue at https://github.com/xtool-org/xtool/issues/new/choose + """ + } + } } diff --git a/Sources/XKit/GrandSlam/Anisette/AnisetteDataProvider.swift b/Sources/XKit/GrandSlam/Anisette/AnisetteDataProvider.swift index 474bbb06..5bca414f 100644 --- a/Sources/XKit/GrandSlam/Anisette/AnisetteDataProvider.swift +++ b/Sources/XKit/GrandSlam/Anisette/AnisetteDataProvider.swift @@ -10,8 +10,14 @@ import Foundation import Dependencies public protocol AnisetteDataProvider: Sendable { + /// Stored alongside login data. If the provider reports a new version, we log out and reset provisioning. + /// + /// The default implementation returns `nil`. + var providerVersion: String? { get } + // This is a suggestion and not a requirement. - func resetProvisioning() async + func resetProvisioning() + func provisioningData() -> ProvisioningData? func fetchAnisetteData() async throws -> AnisetteData @@ -24,8 +30,9 @@ public struct ProvisioningData: Hashable, Codable, Sendable { } extension AnisetteDataProvider { + public var providerVersion: String? { nil } public func provisioningData() -> ProvisioningData? { nil } - public func resetProvisioning() async {} + public func resetProvisioning() {} } extension DependencyValues { diff --git a/Sources/XToolSupport/AuthCommand.swift b/Sources/XToolSupport/AuthCommand.swift index 40f86576..eaa6eea2 100644 --- a/Sources/XToolSupport/AuthCommand.swift +++ b/Sources/XToolSupport/AuthCommand.swift @@ -171,7 +171,7 @@ struct AuthLogoutCommand: AsyncParsableCommand { if reset2FA { @Dependency(\.anisetteDataProvider) var anisetteProvider - await anisetteProvider.resetProvisioning() + anisetteProvider.resetProvisioning() print("Forgot device") } } diff --git a/Sources/XToolSupport/AuthToken.swift b/Sources/XToolSupport/AuthToken.swift index eef02e8e..bf8d564d 100644 --- a/Sources/XToolSupport/AuthToken.swift +++ b/Sources/XToolSupport/AuthToken.swift @@ -9,6 +9,17 @@ enum AuthToken: Codable, CustomStringConvertible { var token: String var expiry: Date var teamID: String + var anisetteVersion: String? + + init(appleID: String, adsid: String, token: String, expiry: Date, teamID: String) { + self.appleID = appleID + self.adsid = adsid + self.token = token + self.expiry = expiry + self.teamID = teamID + @Dependency(\.anisetteDataProvider) var anisetteProvider + self.anisetteVersion = anisetteProvider.providerVersion + } } struct AppStoreConnect: Codable { @@ -58,7 +69,19 @@ extension AuthToken { guard let data = try storage.data(forKey: "XTLAuthToken") else { return nil } - return try decoder.decode(AuthToken.self, from: data) + let decoded = try decoder.decode(AuthToken.self, from: data) + switch decoded { + case .xcode(let xcode): + @Dependency(\.anisetteDataProvider) var anisetteProvider + guard xcode.anisetteVersion == anisetteProvider.providerVersion else { + try? clear() + anisetteProvider.resetProvisioning() + return nil + } + case .appStoreConnect: + break + } + return decoded } static func clear() throws { diff --git a/macOS/Support/XToolMac.entitlements b/macOS/Support/XToolMac.entitlements index 55d91602..5e84acac 100644 --- a/macOS/Support/XToolMac.entitlements +++ b/macOS/Support/XToolMac.entitlements @@ -2,6 +2,8 @@ + com.apple.security.cs.allow-unsigned-executable-memory + keychain-access-groups $(AppIdentifierPrefix)sh.xtool.keychain