import Foundation /// Fehler-Typen des Auth- und Transport-Layers. /// /// Server-Fehler-Codes folgen `AuthErrorCode` aus /// `mana/services/mana-auth/src/lib/auth-errors.ts`. Jeder dort /// definierte Code hat hier einen passenden Case — neue Codes /// auf Server-Seite brauchen einen Minor-Bump dieses Pakets. public enum AuthError: Error, LocalizedError, Sendable, Equatable { // MARK: - Client-Side /// Kein Token im Keychain — User muss sich einloggen. case notSignedIn /// Lokales Encoding (z.B. UTF-8-Konvertierung) gescheitert. case encoding /// Keychain-Operation gescheitert mit OSStatus. case keychain(OSStatus) /// Antwort konnte nicht dekodiert werden. case decoding(String) /// Netzwerk-Schicht hat geworfen. case networkFailure(String) // MARK: - Server-Side (Mapping zu AuthErrorCode in mana-auth) /// `INVALID_CREDENTIALS` — Email oder Passwort falsch. case invalidCredentials /// `EMAIL_NOT_VERIFIED` — Account existiert, Email muss bestätigt /// werden. UI bietet "Bestätigungs-Mail erneut senden" an. case emailNotVerified /// `EMAIL_ALREADY_REGISTERED` — Sign-Up mit existierender Email. case emailAlreadyRegistered /// `WEAK_PASSWORD` — Passwort erfüllt Mindest-Policy nicht. case weakPassword(message: String?) /// `ACCOUNT_LOCKED` — zu viele Fehlversuche. `retryAfter` in Sekunden. case accountLocked(retryAfter: TimeInterval?) /// `SIGNUP_LIMIT_REACHED` — tägliches Registrierungs-Limit aus. case signupLimitReached /// `RATE_LIMITED` — generelles Rate-Limit. `retryAfter` in Sekunden. case rateLimited(retryAfter: TimeInterval?) /// `TOKEN_EXPIRED` — z.B. Reset-Token, Verify-Token. case tokenExpired /// `TOKEN_INVALID` — Token nicht parsebar / nicht gefunden. case tokenInvalid /// `TWO_FACTOR_REQUIRED` — Login erfolgreich, aber 2FA-Challenge offen. case twoFactorRequired /// `TWO_FACTOR_FAILED` — TOTP- oder Backup-Code falsch. case twoFactorFailed /// `PASSKEY_NOT_ENABLED` — User hat keinen Passkey registriert. case passkeyNotEnabled /// `PASSKEY_CANCELLED` — User hat den Plattform-Dialog abgebrochen. case passkeyCancelled /// `PASSKEY_VERIFICATION_FAILED` — WebAuthn-Verifikation gescheitert. case passkeyVerificationFailed /// `VALIDATION` — Request-Body ungültig (z.B. fehlendes Feld). case validation(message: String?) /// `UNAUTHORIZED` — fehlender oder ungültiger Authorization-Header. case unauthorized /// `NOT_FOUND` — referenzierte Ressource existiert nicht. case notFound /// `SERVICE_UNAVAILABLE` — Server temporär nicht erreichbar. case serviceUnavailable /// `INTERNAL` — Server-interner Fehler ohne weitere Klassifikation. case serverInternal /// Fallback für Server-Fehler ohne bekannten `AuthErrorCode`. case serverError(status: Int, code: String?, message: String?) public var errorDescription: String? { switch self { case .notSignedIn: "Nicht angemeldet" case .encoding: "Datenkodierung fehlgeschlagen" case let .keychain(status): "Keychain-Fehler (OSStatus \(status))" case let .decoding(detail): "Antwort konnte nicht gelesen werden: \(detail)" case let .networkFailure(message): "Netzwerkfehler: \(message)" case .invalidCredentials: "Email oder Passwort falsch" case .emailNotVerified: "Bitte bestätige deine Email-Adresse, bevor du dich anmeldest." case .emailAlreadyRegistered: "Diese Email ist bereits registriert." case let .weakPassword(message): message ?? "Passwort erfüllt die Mindest-Anforderungen nicht." case let .accountLocked(retryAfter): retryAfter .map { "Account temporär gesperrt. Erneut versuchen in \(Int($0))s." } ?? "Account temporär gesperrt." case .signupLimitReached: "Das tägliche Registrierungslimit ist erreicht. Versuche es morgen wieder." case let .rateLimited(retryAfter): retryAfter .map { "Zu viele Versuche. Bitte warte \(Int($0))s." } ?? "Zu viele Versuche. Bitte warte einen Moment." case .tokenExpired: "Der Link ist abgelaufen. Bitte fordere einen neuen an." case .tokenInvalid: "Der Link ist ungültig." case .twoFactorRequired: "Zwei-Faktor-Code erforderlich." case .twoFactorFailed: "Zwei-Faktor-Code falsch." case .passkeyNotEnabled: "Für diesen Account ist kein Passkey eingerichtet." case .passkeyCancelled: "Passkey-Eingabe abgebrochen." case .passkeyVerificationFailed: "Passkey konnte nicht verifiziert werden." case let .validation(message): message ?? "Eingabe ungültig." case .unauthorized: "Nicht autorisiert." case .notFound: "Nicht gefunden." case .serviceUnavailable: "Server temporär nicht erreichbar. Bitte später erneut versuchen." case .serverInternal: "Server-Fehler. Bitte später erneut versuchen." case let .serverError(status, code, message): "Server-Fehler (\(status))" + (code.map { " — \($0)" } ?? "") + (message.map { ": \($0)" } ?? "") } } /// `true` wenn dieser Fehler bedeutet, dass die gespeicherten /// Tokens nicht mehr gültig sind und die App den Nutzer ausloggen /// muss. `false` für *transiente* Fehler (Server-Downtime, Netzwerk- /// Fehler, Rate-Limiting), bei denen die Session erhalten bleiben /// soll — die App sollte den Versuch nur wiederholen. /// /// Genutzt von ``AuthClient/refreshAccessToken()`` um zu entscheiden, /// ob der Keychain gewiped werden muss. Apps können denselben /// Test auf Fehler aus ``AuthenticatedTransport`` anwenden, um /// zwischen "neuer Login nötig" und "später nochmal probieren" zu /// unterscheiden. public var invalidatesSession: Bool { switch self { case .invalidCredentials, .unauthorized, .tokenExpired, .tokenInvalid, .emailNotVerified: true case .notSignedIn, .encoding, .keychain, .decoding, .networkFailure, .emailAlreadyRegistered, .weakPassword, .accountLocked, .signupLimitReached, .rateLimited, .twoFactorRequired, .twoFactorFailed, .passkeyNotEnabled, .passkeyCancelled, .passkeyVerificationFailed, .validation, .notFound, .serviceUnavailable, .serverInternal, .serverError: false } } /// Klassifiziert eine `mana-auth`-Fehler-Antwort. `data` ist der /// Response-Body, `status` der HTTP-Status, `retryAfterHeader` der /// `Retry-After`-Header-Wert in Sekunden (falls vorhanden). /// /// Mapping zu `AuthErrorCode` in /// `mana/services/mana-auth/src/lib/auth-errors.ts`. Unbekannte Codes /// fallen auf ``serverError(status:code:message:)`` zurück. public static func classify( status: Int, data: Data, retryAfterHeader: TimeInterval? = nil ) -> AuthError { let body = try? JSONDecoder().decode(ServerErrorBody.self, from: data) let message = body?.message let retryAfter = body?.retryAfterSec ?? retryAfterHeader switch body?.error { case "INVALID_CREDENTIALS": return .invalidCredentials case "EMAIL_NOT_VERIFIED": return .emailNotVerified case "EMAIL_ALREADY_REGISTERED": return .emailAlreadyRegistered case "WEAK_PASSWORD": return .weakPassword(message: message) case "ACCOUNT_LOCKED": return .accountLocked(retryAfter: retryAfter) case "SIGNUP_LIMIT_REACHED": return .signupLimitReached case "RATE_LIMITED": return .rateLimited(retryAfter: retryAfter) case "TOKEN_EXPIRED": return .tokenExpired case "TOKEN_INVALID": return .tokenInvalid case "TWO_FACTOR_REQUIRED": return .twoFactorRequired case "TWO_FACTOR_FAILED": return .twoFactorFailed case "PASSKEY_NOT_ENABLED": return .passkeyNotEnabled case "PASSKEY_CANCELLED": return .passkeyCancelled case "PASSKEY_VERIFICATION_FAILED": return .passkeyVerificationFailed case "VALIDATION": return .validation(message: message) case "UNAUTHORIZED": return .unauthorized case "NOT_FOUND": return .notFound case "SERVICE_UNAVAILABLE": return .serviceUnavailable case "INTERNAL": return .serverInternal default: // Status-Heuristik wenn kein Code geliefert wurde. switch status { case 401: return .invalidCredentials case 403: return .emailNotVerified case 404: return .notFound case 429: return .rateLimited(retryAfter: retryAfter) case 503: return .serviceUnavailable default: return .serverError(status: status, code: body?.error, message: message) } } } } /// Wire-Format der `mana-auth`-Fehler-Antwort. Spiegelt /// `AuthErrorResponseBody` aus `lib/auth-errors.ts`. struct ServerErrorBody: Decodable, Sendable { let error: String? let message: String? let status: Int? let retryAfterSec: TimeInterval? }