import Foundation /// App-spezifische Konfiguration für ManaCore. Wird von der konsumierenden /// App beim Erzeugen eines `AuthClient` injiziert. /// /// ManaCore hardcoded nichts App-Spezifisches. Bundle-ID, Auth-Server-URL /// und Keychain-Adressierung kommen ausschließlich hierüber. public protocol ManaAppConfig: Sendable { /// Basis-URL des mana-auth-Servers, z.B. `https://auth.mana.how`. var authBaseURL: URL { get } /// Keychain-Service-Identifier, üblich `ev.mana.`. Trennt /// Token-Einträge verschiedener Apps voneinander, falls keine /// shared Access-Group benutzt wird. var keychainService: String { get } /// Optional: Shared-Keychain-Access-Group für Cross-App-SSO. /// `nil` bedeutet: nur App-eigener Keychain-Zugriff. /// /// Wenn gesetzt, müssen alle teilnehmenden Apps unter derselben /// Apple-Developer-Team-ID provisioniert sein und das Entitlement /// `keychain-access-groups` mit demselben Wert tragen. var keychainAccessGroup: String? { get } /// App-Group für Daten-Sharing zwischen App ↔ Widget ↔ ShareExt. /// Üblich `group.ev.mana.`. `nil` für Apps ohne Extensions. /// /// Single-Source für den App-Group-String, der heute in jeder App /// 3-4× hardcoded steht (AppConfig + App-Entitlement + Widget- /// Entitlement + ShareExt-Entitlement). Die Entitlements bleiben /// hardcoded (das verlangt iOS), aber im Swift-Code ist der Wert /// damit single-source. var appGroup: String? { get } /// OSLog-Subsystem für App-Logger, üblich `ev.mana.`. Default /// ist `keychainService` (der schon der Konvention folgt). var logSubsystem: String { get } /// Was ``AuthClient/refreshAccessToken()`` macht, wenn der Server /// einen Session-invalidierenden Fehler zurückgibt (401, tokenExpired, /// tokenInvalid, ...). Default ``RefreshFailurePolicy/immediateWipe`` /// für Quellkompatibilität mit allen bestehenden Apps. /// /// Apps, die einen TestFlight-/Cold-Launch-Logout durch eine /// transiente Server-/Deployment-Glitch verhindern wollen, setzen /// ``RefreshFailurePolicy/softFirst`` — dann überlebt die persistierte /// Session den ersten Refresh-Fehler im Prozess und wird erst gewiped, /// wenn der Server beim nächsten Versuch nochmal "Session tot" sagt /// (oder wenn vorher schon ein erfolgreicher Refresh in diesem /// Prozess passiert ist — dann ist der invalidate-Response /// vertrauenswürdig). var refreshFailurePolicy: RefreshFailurePolicy { get } } // MARK: - Default-Implementationen public extension ManaAppConfig { /// Default `nil` — Apps ohne Widget/ShareExt müssen nichts setzen. var appGroup: String? { nil } /// Default = `keychainService`. Beide folgen heute in allen Apps /// derselben Konvention `ev.mana.`. var logSubsystem: String { keychainService } /// Default `immediateWipe` — bestehendes Verhalten. var refreshFailurePolicy: RefreshFailurePolicy { .immediateWipe } } /// Standard-Implementierung von ``ManaAppConfig``. Apps können diese /// nutzen oder ein eigenes Type adoptieren. public struct DefaultManaAppConfig: ManaAppConfig { public let authBaseURL: URL public let keychainService: String public let keychainAccessGroup: String? public let appGroup: String? public let logSubsystem: String public let refreshFailurePolicy: RefreshFailurePolicy public init( authBaseURL: URL, keychainService: String, keychainAccessGroup: String? = nil, appGroup: String? = nil, logSubsystem: String? = nil, refreshFailurePolicy: RefreshFailurePolicy = .immediateWipe ) { self.authBaseURL = authBaseURL self.keychainService = keychainService self.keychainAccessGroup = keychainAccessGroup self.appGroup = appGroup // Konvention: log-Subsystem = keychainService, falls nicht // explizit anders gewünscht. self.logSubsystem = logSubsystem ?? keychainService self.refreshFailurePolicy = refreshFailurePolicy } } /// Policy für ``AuthClient/refreshAccessToken()``-Verhalten bei /// Session-invalidierenden Server-Antworten. /// /// `immediateWipe` ist das historische Verhalten von ManaCore: jeder /// Server-Hinweis "Session tot" → Keychain wipe → User wird ausgeloggt. /// Problem: ein transienter Server-Bug (z.B. mana-auth-Regression /// 2026-05-19, siehe `project_auth_refresh_bug` in der Memory) kann /// dann **alle** ManaCore-Apps gleichzeitig auswerfen. /// /// `softFirst` macht den ersten Refresh-Fehler eines Prozesses zu einem /// "Vielleicht" — Session bleibt im Keychain, App kann es beim nächsten /// Request nochmal probieren. Erst der **zweite** Fehler in Folge /// (oder ein Fehler nach einem zuvor erfolgreichen Refresh im selben /// Prozess) löst den Wipe aus. /// /// Trade-off: bei `softFirst` sieht ein User mit echt invalider /// Session beim ersten Request einen Auth-Fehler statt direkt im /// Login-Screen zu landen. Akzeptabel — der zweite Request wiped /// dann sauber und User landet im Login. public enum RefreshFailurePolicy: Sendable { /// Default — Server-"Session-tot"-Antworten führen sofort zu /// `keychain.wipe()` und Status `.signedOut`. case immediateWipe /// Erster invalidierender Refresh-Fehler im Prozess wird **nicht** /// gewiped — Session bleibt erhalten, Fehler wird geworfen. Wipe /// passiert beim zweiten Fehler oder nach mindestens einem /// erfolgreichen Refresh in diesem Prozess. case softFirst }