import Foundation import Combine /// Orchestrates authentication and owns the authenticated session. /// /// **Philosophy:** Auth establishes *account* identity (who you are to the /// app) — it never, by itself, reveals anything to other users. Social /// identity is a separate, opt-in concern handled by `ProfileImportService` /// at reveal time. This separation is what keeps the "anonymous until mutual /// consent" promise intact. @MainActor final class AuthService: ObservableObject { @Published private(set) var state: AuthState = .signedOut @Published private(set) var account: Account? private let providers: [AuthProviderID: AuthProvider] private let apiClient: APIClient private let keychain: KeychainStore init( apiClient: APIClient, keychain: KeychainStore = KeychainStore(), providers: [AuthProviderID: AuthProvider] = [ .apple: AppleAuthProvider(), .google: GoogleAuthProvider() ] ) { self.apiClient = apiClient self.keychain = keychain self.providers = providers } /// Restore a previously authenticated session from the keychain. func restoreSession() async { guard let session = keychain.loadSession() else { return } state = .authenticated(session) account = session.account } /// Sign in with a given provider. func signIn(with providerID: AuthProviderID) async throws { guard let provider = providers[providerID] else { throw AuthError.providerUnavailable } state = .authenticating do { let result = try await provider.signIn() // Exchange the provider ID token for a Proximity session token. let session = try await apiClient.exchangeToken(result) keychain.save(session) state = .authenticated(session) account = session.account } catch { state = .signedOut throw error } } /// Sign out locally and revoke the backend session. func signOut() async { if case let .authenticated(session) = state { await apiClient.revokeSession(session) } keychain.clearSession() state = .signedOut account = nil } } /// The user's authenticated account (account identity only). struct Account: Codable, Hashable { let id: String let providerID: AuthProviderID let displayName: String let email: String? } /// The session state machine. enum AuthState: Equatable { case signedOut case authenticating case authenticated(Session) static func == (lhs: AuthState, rhs: AuthState) -> Bool { switch (lhs, rhs) { case (.signedOut, .signedOut): return true case (.authenticating, .authenticating): return true case (.authenticated(let a), .authenticated(let b)): return a.token == b.token default: return false } } } /// A verified Proximity session. struct Session: Codable { let token: String let account: Account }