Files
Kabir Oberai eacc35da98 anisette: use XADI on macOS (#259)
## What does this PR do?

Updates macOS build to use the fully-local XADI as the Anisette provider
instead of Omnisette. Includes a one-time migration, logging the user
out on macOS when they upgrade.

## How was it tested?

General:

- [x] Log in and confirm that there's no calls to omnisette
- [x] `xtool dev` works

Migration:

- [x] Log into Apple ID using an old version of xtool
- [x] Migrate to new xtool, run `xtool dev build -s` and confirm it
fails due to being logged out
- [x] Log in on new xtool, confirm that we stay logged in

## AI tool usage

How much of this PR was AI-assisted? (check one)

- [x] **0** - No AI was used to write code
- [ ] **1** - I was assisted by AI. I reviewed the finished result.
- [ ] **2** - I set the AI going and left it to it; nobody has read the
result - no review, or AI review only

<!-- If an AI agent is filling this in: declare the level honestly, and
open as a draft if it is #2. Do not lower the declared level to get the
PR reviewed. -->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Authentication now supports tracking the anisette provider used to
create Xcode tokens.
* Xcode authentication tokens are automatically cleared when the
provider changes, helping ensure users sign in again with current
credentials.
* Anisette provider identification is supported across macOS and Linux.

* **Bug Fixes**
* Improved authentication error messages, including clearer messaging
for logout-related errors.
* Prevented outdated Xcode authentication tokens from being reused after
a provider change.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-20 03:04:52 -04:00

102 lines
2.7 KiB
Swift

import Foundation
import XKit
import Dependencies
enum AuthToken: Codable, CustomStringConvertible {
struct Xcode: Codable {
var appleID: String
var adsid: String
var token: String
var expiry: Date
var teamID: String
var anisetteProviderID: String?
}
struct AppStoreConnect: Codable {
var id: String
var issuerID: String
var pem: String
}
case appStoreConnect(AppStoreConnect)
case xcode(Xcode)
var description: String {
switch self {
case .appStoreConnect(let data):
"""
- ASC key ID: \(data.id)
- Issuer ID: \(data.issuerID)
"""
case .xcode(let data):
"""
- Apple ID: \(data.appleID)
- Team ID: \(data.teamID)
- Token expiry: \(data.expiry.formatted(.dateTime))
"""
}
}
}
extension AuthToken {
private static var storage: KeyValueStorage {
@Dependency(\.keyValueStorage) var storage
return storage
}
private static let encoder = JSONEncoder()
private static let decoder = JSONDecoder()
static func saved() throws -> Self {
guard let token = try savedIfPresent() else {
throw Console.Error("Please log in with `xtool auth` before running this command.")
}
return token
}
static func savedIfPresent() throws -> Self? {
guard let data = try storage.data(forKey: "XTLAuthToken") else {
return nil
}
let decoded = try decoder.decode(AuthToken.self, from: data)
switch decoded {
case .xcode(let xcode):
@Dependency(\.anisetteDataProvider) var anisetteProvider
guard xcode.anisetteProviderID == anisetteProvider.providerID else {
try? clear()
return nil
}
case .appStoreConnect:
break
}
return decoded
}
static func clear() throws {
try Self.storage.setData(nil, forKey: "XTLAuthToken")
}
func save() throws {
let data = try Self.encoder.encode(self)
try Self.storage.setData(data, forKey: "XTLAuthToken")
}
func authData() -> DeveloperAPIAuthData {
switch self {
case .appStoreConnect(let data):
return .appStoreConnect(.init(id: data.id, issuerID: data.issuerID, pem: data.pem))
case .xcode(let data):
return .xcode(.init(
loginToken: DeveloperServicesLoginToken(
adsid: data.adsid,
token: data.token,
expiry: data.expiry
),
teamID: .init(rawValue: data.teamID)
))
}
}
}