Audit 2026-05-17 V4. Ersetzt das hand-getippte Log.swift-Boilerplate
in jeder App durch einen Config-getriebenen Wrapper.
Neu:
- `ManaAppLog` — Factory fuer OSLog-Logger gegen ein ManaAppConfig.
Standard-Kategorien app/auth/api/db/web, plus `category("…")` fuer
app-spezifische Kategorien.
- `ManaAppConfig.appGroup: String?` (default nil) — Single-Source fuer
den App-Group-String, der heute in jeder App 3-4× hardcoded steht.
- `ManaAppConfig.logSubsystem: String` (default = keychainService) —
Subsystem fuer ManaAppLog.
Nichts breaking — beide neuen Felder haben Default-Implementations,
DefaultManaAppConfig.init hat zwei zusaetzliche optionale Parameter.
Tests: 4 neue ManaAppConfig-Tests + 5 neue ManaAppLog-Tests.
85/85 gruen (vorher 76/76).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
17 KiB
Changelog
Alle Änderungen werden hier dokumentiert. Format orientiert an Keep a Changelog, Versionierung nach Semver.
[1.7.0] — 2026-05-17
Minor — ManaAppLog + ManaAppConfig.appGroup/logSubsystem.
Audit 2026-05-17 V4. Ersetzt das hand-getippte Log.swift-Boilerplate
in jeder App durch einen Config-getriebenen Wrapper.
Neu
ManaAppLog(public struct, Sendable) — Factory für OSLog-Logger gegen einManaAppConfig. Standard-Kategorienapp/auth/api/db/web, pluscategory("…")für app-spezifische Kategorien.ManaAppConfig.appGroup: String?(defaultnil) — Single-Source für den App-Group-String, der heute in jeder App 3-4× hardcoded steht. Apps ohne Widget/ShareExt setzen weiterhin nichts.ManaAppConfig.logSubsystem: String(default =keychainService) — Subsystem fürManaAppLog. In allen Apps heute schonev.mana.<app>, deshalb default sinnvoll.
Geändert
- Nichts breaking. Beide neuen Felder haben Default-Implementations
im Protocol-Extension, bestehende Konsumenten von
ManaAppConfigbrauchen nichts anzupassen. DefaultManaAppConfig.inithat zwei zusätzliche optionale Parameter (appGroup,logSubsystem), beide mitnil-Defaults — Quellkompatibel.
Tests
- 4 neue ManaAppConfig-Tests + 5 neue ManaAppLog-Tests. 85/85 grün (vorher 76/76).
Migrations-Hinweis
Apps können ihre lokale Log.swift von ~13 LOC auf ~5 LOC schrumpfen:
import ManaCore
enum Log {
private static let mana = ManaAppLog(AppConfig.manaAppConfig)
static let app = mana.app
static let auth = mana.auth
static let api = mana.api
static let study = mana.category("study") // app-spezifisch
}
Plus AppConfig.manaAppConfig kann appGroup: "group.ev.mana.<app>"
ergänzen, damit der App-Group-String single-source ist.
[1.6.0] — 2026-05-17
Minor — ManaTheme-Variants in ManaTokens. Acht Web-Themes aus
@mana/themes (mana, forest, paper, neutral, lume, twilight,
skylight, monochrome) sind jetzt als Swift verfügbar; generiert aus
den CSS-Quellen via pnpm --filter @mana/themes gen:swift.
Hintergrund: bisher haben Cards, Manaspur und Nutriphi je ~90 LOC forest-HSL-Apparat lokal nachgebaut, weil ManaTokens nur die mana-Variant kannte. Mit v1.6.0 sind diese App-lokalen Theme-Files ablösbar (Audit 2026-05-17 Vorschlag V1).
Neu
ManaTheme(public enum) —case mana | forest | paper | neutral | lume | twilight | skylight | monochrome.String-RawValue,CaseIterable,Sendable.ManaThemeColors(public struct, Sendable) — die 12 Tokens alsColor-Properties. Per-Variant statisch verfügbar (ManaThemeColors.forestusw.) aus der generierten DateiGeneratedThemes.swift.ManaTheme.colors+ Convenience-Accessoren (.background,.foreground, …) — beide Schreibweisen funktionieren.EnvironmentValues.manaTheme+View.manaTheme(_:)— SwiftUI-Environment, Default.mana. Apps mit User-Theme-Switching setzen den Wert per@AppStorage-gestütztem Binding.
Geändert
- Nichts breaking.
ManaColor.*undManaBrand.*bleiben unverändert und liefern weiter die mana-Variant-Werte.
Generator
mana/packages/themes/scripts/gen-swift-themes.mjsliest die acht CSS-Variant-Dateien und schreibtGeneratedThemes.swift. CI-Drift- Check: nach Generator-Laufgit diff --exit-codein beiden Repos.
Migrations-Hinweis
Apps können ihre lokalen *Theme.swift-Files (Cards, Manaspur,
Nutriphi) durch direkten Aufruf von ManaTheme.<variant> ersetzen.
Konvention: Apps mit fester Identität setzen .manaTheme(.<variant>)
an der App-Root; verschachtelte Views lesen via
@Environment(\.manaTheme).
Tests
- 7 neue Tests (
ThemeTests.swift). 12/12 ManaTokens grün auf macOS.
[1.5.1] — 2026-05-17
Patch — KeychainStore-Migration-Fallback. Bisher haben die mana-Apps
keychainAccessGroup: nil gesetzt; Apple legt das Item dann im
default-bucket ($(AppIdentifierPrefix).$(BundleId)) ab. Beim
Wechsel auf eine explizite accessGroup (Apps werden mit v1.5.1
nachgezogen) hätten User sonst beim ersten Start einen Logout
gesehen, weil Apples Read-mit-Group das alte Item nicht immer
liefert.
Verändert
KeychainStore.getString(for:)— beiaccessGroup != nilund einem Miss wird einmalig ohnekSecAttrAccessGroupre-queryt. Findet sich der alte Eintrag im default-bucket, wird er in den expliziten Bucket migriert und der alte gelöscht. Transparent für alle Caller —getStringliefert wie gewohnt den Wert zurück.
Migrations-Hinweis
- Apps, die
keychainAccessGroupab v1.5.1 explizit setzen, brauchen keinen Logout zu erwarten. Apps, die weiterhinnilsetzen, sind von dem Fallback nicht betroffen (er greift nur beiaccessGroup != nil).
[1.5.0] — 2026-05-14
Minor — getProfile() + ProfileInfo. Apps können den 2FA-Status
des eingeloggten Users lesen, damit AccountView entscheidet ob
"Aktivieren" oder "Deaktivieren" angezeigt wird.
Neu
ProfileInfo(public struct) —id,email,name,emailVerified,twoFactorEnabled.AuthClient.getProfile() -> ProfileInfo— lädt aktuelles Profil vom Server (GET /api/v1/auth/profile→ Better Auths/api/auth/get-session). Nutzt Session-Token als Bearer.
Tests
- 4 neue Tests (twoFactor-on, twoFactor-off, ohne Session, unauthorized). 70/70 grün.
[1.4.0] — 2026-05-14
Minor — 2FA-Enrollment (Mini-Sprint B). Setzt Mini-Sprint A
(v1.3.0) voraus. Komplett additiv.
ManaCore — 2FA-Enrollment
TotpEnrollment(public struct) —totpURI(für QR-Code-Display)backupCodes(Liste).
AuthClient.enrollTotp(password:) -> TotpEnrollment— aktiviert TOTP-2FA; Server generiert Secret + Backup-Codes.AuthClient.disableTotp(password:)— deaktiviert wieder.AuthClient.getTotpUri(password:) -> String— Re-Display für zweites Authenticator-Gerät.AuthClient.regenerateBackupCodes(password:) -> [String]— neue Codes, alte werden ungültig.
Alle vier Methoden senden Bearer-Header mit Session-Token (Wire- Konvention für mana-auth-Account-Endpoints).
Server-Side Voraussetzung
mana-auth ≥ Commit der Wrapper-Endpoints:
POST /api/v1/auth/two-factor/enablePOST /api/v1/auth/two-factor/disablePOST /api/v1/auth/two-factor/get-totp-uriPOST /api/v1/auth/two-factor/generate-backup-codes
Tests
- 7 neue Tests (Success-Pfade aller vier Methoden, leeres Passwort, ohne Session, falsches Passwort). 66/66 grün.
[1.3.0] — 2026-05-14
Minor — 2FA-Login-Challenge (Mini-Sprint A). Apps mit aktiviertem TOTP-2FA können sich jetzt nativ einloggen. Komplett additiv.
ManaCore — 2FA-Login
AuthClient.Status.twoFactorRequired(token: String, methods: [String], email: String)als neuer Case. Tritt nachsignIn(...)auf, wenn der Account 2FA aktiviert hat.tokenist der opaquetwo_factor-Cookie-Wert vom Server, den die App beiverifyTotp(...)zurückschickt.AuthClient.verifyTotp(code:trustDevice:)— verifiziert 6-stelligen TOTP-Code. Bei Erfolg.signedIn, bei Fehler bleibt der Status im Challenge (User kann retry).AuthClient.verifyBackupCode(code:trustDevice:)— Fallback wenn das TOTP-Gerät verloren wurde. Backup-Codes sind einmalig.signIn(...)erkennt den Server-Pfad{twoFactorRequired: true, ...}und routet automatisch zu.twoFactorRequired.
Server-Side Voraussetzung
Setzt zwei neue Custom-Endpoints in mana-auth voraus:
POST /api/v1/auth/two-factor/verify-totpPOST /api/v1/auth/two-factor/verify-backup-code
Plus die /api/v1/auth/login-Erweiterung um den twoFactorRequired-
Pfad. Siehe mana/services/mana-auth/src/routes/auth.ts.
Tests
- 5 neue Tests (signIn-Redirect, verifyTotp-Success/-Fail, ohne-Challenge- Guard, verifyBackupCode). 59/59 grün.
Bewusst NICHT in v1.3.0
- 2FA-Enrollment (TOTP-Setup) — eigener Mini-Sprint B mit
enrollTotp(),disableTotp(),regenerateBackupCodes(). - Magic-Link, Passkey — eigene Sprints.
[1.2.0] — 2026-05-13
Minor — Guest-Mode + Auth-Resilience. Native-Apps werden gegen mana-auth- Downtime gehärtet und können jetzt einen anonymen Local-First-Modus anbieten. Komplett additiv — keine Breaking Changes für bestehende Konsumenten (Memoro, Cards, Manaspur, Nutriphi).
ManaCore — Guest-Identität
AuthClient.Statusum Case.guest(id: String)erweitert. Persistente lokale UUID ohne Server-Account; gleichberechtigt mit.signedInals „App ist nutzbar"-Zustand. Apps können in diesem Modus alles Lokale und alle unauthenticated-Server-Endpoints anbieten, schreibende Endpoints poppen Auth-Sheet.AuthClient.enterGuestMode() throws -> String— idempotent, erzeugt oder reuse die Guest-UUID aus Keychain. Wechselt den Status nur, wenn aktuell.signedOut/.unknown(eine aktive Session bleibt unangetastet, App kann die Guest-ID parallel lesen).AuthClient.currentGuestId() -> String?— Lookup unabhängig vom Status. Genutzt z.B. um lokale Guest-Daten beim Sign-In dem neuen Server- Account zuzuordnen.AuthClient.clearGuestId()— entfernt die Guest-ID, etwa nach erfolgreicher Migration der lokalen Daten auf einen Server-Account.AuthClient.signOut(keepGuestMode: Bool = false)— Default-Verhalten unverändert (falselöscht alles, Status.signedOut). Mittruebleibt die App im anonymen Modus weiter nutzbar.KeychainStore.Key.guestIdals neuer Key.wipe()löscht jetzt nur Session-Felder (accessToken/refreshToken/email) — die Guest-ID überlebt. Für komplettes Vergessen: neuewipeAll().
ManaCore — Refresh-Resilience
refreshAccessToken()wipt nicht mehr blind den Keychain bei jedem Nicht-200. Stattdessen Heuristik viaAuthError.invalidatesSession:- Wipe bei
.invalidCredentials,.unauthorized,.tokenExpired,.tokenInvalid,.emailNotVerified— Session ist tatsächlich tot. - Behalten bei
.serviceUnavailable(503),.serverInternal(500),.networkFailure,.rateLimited, weiteren transienten Fehlern. Apps werden bei mana-auth-Downtime nicht mehr in den Login-Screen geworfen.
- Wipe bei
- Beim Wipe-Pfad fällt der Status auf
.guest(id)zurück, falls eine Guest-Identität existiert; sonst auf.signedOut. AuthError.invalidatesSession: Bool— public computed Property, auch von Apps direkt nutzbar (z.B. um auf Transport-Fehler zu reagieren).
Tests
- 15 neue Tests: Guest-Mode (Idempotenz, Bootstrap-Priorität, Status- Übergänge), signOut(keepGuestMode:) in beiden Modi, Refresh-Verhalten bei 401/429/500/503/Network, invalidatesSession-Partitionierung.
Migration für Apps
Bestehende Apps brauchen keine Änderung — Default-Verhalten ist identisch. Wer den anonymen Modus nutzen will:
// Beim App-Start nach bootstrap():
auth.bootstrap()
if case .signedOut = auth.status {
try? auth.enterGuestMode() // Statt sofort Login-Screen
}
// In Aktionen, die einen Account brauchen:
guard case .signedIn = auth.status else {
presentLoginSheet()
return
}
[1.1.1] — 2026-05-13
Patch — Wire-Konvention für authenticated Account-Calls geklärt.
Geändert
AuthClient.changeEmail,changePassword,deleteAccountsenden jetzt den Session-Token (refreshToken-Feldwert) statt des JWT alsAuthorization: Bearer. Hintergrund: Server-seitig wurde inmana-authBetter Authsbearer-Plugin aktiviert (requireSignature: false), das Session-Tokens zu Session-Cookies konvertiert. Damit funktionierenauth.api.changeEmailetc. für Native-Apps ohne Cookie-Container.AuthClient.currentSessionToken()als public Helper hinzu. Symmetrisch zucurrentAccessToken().
Trade-Off bewusst akzeptiert
Session-Token wird bei jedem Account-Call versendet (vorher nur beim
/refresh). Mit TLS-Baseline akzeptables Risiko; Compromise-Surface
nicht relevant größer als JWT-Leak. Alternative wäre ein Custom-
Bearer-JWT-to-Cookie-Resolver im Server (40+ Zeilen Hono-Middleware,
HMAC-Cookie-Synthese) — bewusst nicht gewählt, weil der bearer-Plugin
genau für diesen Use-Case existiert.
Tests
- Test
changePassword schickt Bearer-Headerumbenannt aufschickt Session-Token als Bearer (nicht JWT)und geupdated.
[1.1.0] — 2026-05-13
Phase 1 aus dem Native-Auth-Vollausbau-Plan (Option A — alles nativ,
siehe mana/docs/MANA_SWIFT.md). Erweitert ManaCore um die
Account-Lifecycle-Methoden, die jede native Verein-App für eine
vollständige Auth-Reise braucht.
ManaCore — Neue API (additiv, keine Breaking Changes)
AuthClient.register(email:password:name:sourceAppUrl:)— Sign-Up gegenPOST /api/v1/auth/register. Persistiert eine Session automatisch, wenn der Server Tokens mitliefert; sonst still und wartend auf Email-Verifikation.AuthClient.forgotPassword(email:resetUniversalLink:)— Passwort- Reset-Mail anfordern gegenPOST /api/v1/auth/forgot-password. Server antwortet immer 200 (keine User-Enumeration).AuthClient.resetPassword(token:newPassword:)— Passwort mit Token aus Reset-Mail setzen.AuthClient.resendVerification(email:sourceAppUrl:)— Verify-Mail erneut versenden, aufzurufen nachAuthError/emailNotVerified.AuthClient.changeEmail(newEmail:callbackUniversalLink:)— Email ändern (verschickt Verify-Mail an neue Adresse). Aktuell server- seitig nicht Bearer-fähig — siehe Doc-Header vonAuthClient+Account.swift.AuthClient.changePassword(currentPassword:newPassword:)— Passwort ändern. Gleiche Bearer-Einschränkung wiechangeEmail.AuthClient.deleteAccount(password:)— Account löschen (App-Store-Guideline 5.1.1(v) Pflicht). Wiped Keychain bei Erfolg. Gleiche Bearer-Einschränkung wie oben.
ManaCore — AuthError ausgebaut
- Präzise Cases pro Server-
AuthErrorCode:.emailNotVerified,.emailAlreadyRegistered,.weakPassword(message:),.accountLocked(retryAfter:),.signupLimitReached,.rateLimited(retryAfter:),.tokenExpired,.tokenInvalid,.twoFactorRequired,.twoFactorFailed,.passkeyNotEnabled,.passkeyCancelled,.passkeyVerificationFailed,.validation(message:),.unauthorized,.notFound,.serviceUnavailable,.serverInternal. AuthError.classify(status:data:retryAfterHeader:)— public, klassifiziert mana-auth-Fehler-Antworten in den passenden Case. Auch genutzt vonsignInundrefreshAccessToken(vorher: einfache.error(String)-Strings).AuthErrorist jetztEquatable— erleichtert UI-Logik und Tests.- Alte Cases
.invalidCredentials,.networkFailure,.encoding,.keychain,.decoding,.notSignedInbleiben unverändert. - Breaking-Vermeidung:
serverError(status:message:)wurde zuserverError(status:code:message:)(zusätzlichescode-Argument). Theoretisch breaking, praktisch nutzt es niemand außerhalb von ManaCore selbst. Wenn ein App-Konsument darauf gepattern-matched hat, ist das ein Compile-Fehler, kein Runtime-Bug.
Tests
- 14 neue Tests für
AuthError.classify(jeder ErrorCode + Status- Heuristik + Retry-After-Header + kaputter Body). - 12 neue Tests für die neuen
AuthClient-Methoden viaURLProtocol-Mock (Wire-Format, Status-Mapping, Bearer-Header, Session-Persistenz beiregister, Session-Wipe beideleteAccount).
Bekannte Einschränkungen
changeEmail,changePassword,deleteAccountbrauchen Server- seitig denbearer-Plugin von Better Auth oder einen Custom- Bearer-Resolver. Heute mountetmana-authnur den Cookie-Pfad. Phase-3-Server-PR immana-Repo dokumentiert.- 2FA-Verify, Magic-Link und Passkey-Flows sind in dieser Version
bewusst NICHT enthalten. Laufen Server-seitig über Better-Auth-
Native (
/api/auth/*, Cookie) und brauchen eigene JWT-Pfade. Folgt in v1.2.0 zusammen mit dem Server-PR.
[1.0.1] — 2026-05-13
Behoben
AuthenticatedTransport:URL.appending(path:)URL-encoded das?in Query-Strings zu%3F, was den Server-Route-Match brechen ließ (404 für/healthz?…). Ersetzt durch String-Concat; Caller liefert den Path inkl. führendem/und optionaler Query.
[1.0.0] — 2026-05-12
Initiale Extraktion aus memoro-native (Phase α aus
mana/docs/MANA_SWIFT.md).
ManaCore (neu)
ManaAppConfig-Protocol für App-injizierbare Konfiguration (authBaseURL,keychainService,keychainAccessGroup).AuthClient— mana-auth-Login per E-Mail+PW, Status-Maschine, Token-Speicherung im Keychain, proaktiver Refresh.JWT— Token-Expiry-Berechnung (lokaler Parse, keine Signatur-Verifikation).KeychainStore— generisches Token-Storage, konfigurierbarer Service-Identifier + Access-Group.AuthError— sprechende Fehlertypen mitLocalizedError-Texten.AuthenticatedTransport— URLSession-Wrapper mit Auth-Header und automatischem 401-Retry-mit-Refresh.
ManaTokens (neu)
- Farben, Spacings, Typography, Radius — gespiegelt aus
mana/docs/THEMING.md.