mana-swift-core/CHANGELOG.md
Till JS 4ce22ac74e feat(core): ManaAppLog + appGroup/logSubsystem in ManaAppConfig (v1.7.0)
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>
2026-05-17 22:23:03 +02:00

427 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Changelog
Alle Änderungen werden hier dokumentiert. Format orientiert an
[Keep a Changelog](https://keepachangelog.com), Versionierung nach
[Semver](https://semver.org).
## [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 ein `ManaAppConfig`. Standard-Kategorien `app`/`auth`/`api`/
`db`/`web`, plus `category("…")` für app-spezifische Kategorien.
- `ManaAppConfig.appGroup: String?` (default `nil`) — 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ür `ManaAppLog`. In allen Apps heute schon
`ev.mana.<app>`, deshalb default sinnvoll.
### Geändert
- Nichts breaking. Beide neuen Felder haben Default-Implementations
im Protocol-Extension, bestehende Konsumenten von `ManaAppConfig`
brauchen nichts anzupassen.
- `DefaultManaAppConfig.init` hat zwei zusätzliche optionale Parameter
(`appGroup`, `logSubsystem`), beide mit `nil`-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:
```swift
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 als
`Color`-Properties. Per-Variant statisch verfügbar
(`ManaThemeColors.forest` usw.) aus der generierten Datei
`GeneratedThemes.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.*` und `ManaBrand.*` bleiben unverändert
und liefern weiter die mana-Variant-Werte.
### Generator
- `mana/packages/themes/scripts/gen-swift-themes.mjs` liest die acht
CSS-Variant-Dateien und schreibt `GeneratedThemes.swift`. CI-Drift-
Check: nach Generator-Lauf `git diff --exit-code` in 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:)` — bei `accessGroup != nil` und
einem Miss wird einmalig ohne `kSecAttrAccessGroup` re-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 — `getString` liefert wie gewohnt den Wert zurück.
### Migrations-Hinweis
- Apps, die `keychainAccessGroup` ab v1.5.1 explizit setzen, brauchen
keinen Logout zu erwarten. Apps, die weiterhin `nil` setzen, sind
von dem Fallback nicht betroffen (er greift nur bei `accessGroup
!= 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/enable`
- `POST /api/v1/auth/two-factor/disable`
- `POST /api/v1/auth/two-factor/get-totp-uri`
- `POST /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 nach `signIn(...)` auf, wenn der Account 2FA
aktiviert hat. `token` ist der opaque `two_factor`-Cookie-Wert vom
Server, den die App bei `verifyTotp(...)` 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-totp`
- `POST /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.Status` um Case `.guest(id: String)` erweitert. Persistente
lokale UUID ohne Server-Account; gleichberechtigt mit `.signedIn` als
„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 (`false` löscht alles, Status `.signedOut`). Mit `true`
bleibt die App im anonymen Modus weiter nutzbar.
- `KeychainStore.Key.guestId` als neuer Key. `wipe()` löscht jetzt
*nur* Session-Felder (accessToken/refreshToken/email) — die Guest-ID
überlebt. Für komplettes Vergessen: neue `wipeAll()`.
### ManaCore — Refresh-Resilience
- `refreshAccessToken()` wipt nicht mehr blind den Keychain bei jedem
Nicht-200. Stattdessen Heuristik via `AuthError.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.
- 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:
```swift
// 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`, `deleteAccount` senden
jetzt den Session-Token (`refreshToken`-Feldwert) statt des JWT als
`Authorization: Bearer`. Hintergrund: Server-seitig wurde in
`mana-auth` Better Auths `bearer`-Plugin aktiviert
(`requireSignature: false`), das Session-Tokens zu Session-Cookies
konvertiert. Damit funktionieren `auth.api.changeEmail` etc. für
Native-Apps ohne Cookie-Container.
- `AuthClient.currentSessionToken()` als public Helper hinzu. Symmetrisch
zu `currentAccessToken()`.
### 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-Header` umbenannt auf
`schickt 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
gegen `POST /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 gegen `POST /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 nach ``AuthError/emailNotVerified``.
- `AuthClient.changeEmail(newEmail:callbackUniversalLink:)` — Email
ändern (verschickt Verify-Mail an neue Adresse). **Aktuell server-
seitig nicht Bearer-fähig** — siehe Doc-Header von
`AuthClient+Account.swift`.
- `AuthClient.changePassword(currentPassword:newPassword:)` — Passwort
ändern. Gleiche Bearer-Einschränkung wie `changeEmail`.
- `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 von `signIn` und `refreshAccessToken` (vorher: einfache
`.error(String)`-Strings).
- `AuthError` ist jetzt `Equatable` — erleichtert UI-Logik und Tests.
- Alte Cases `.invalidCredentials`, `.networkFailure`, `.encoding`,
`.keychain`, `.decoding`, `.notSignedIn` bleiben unverändert.
- **Breaking-Vermeidung:** `serverError(status:message:)` wurde zu
`serverError(status:code:message:)` (zusätzliches `code`-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 via
`URLProtocol`-Mock (Wire-Format, Status-Mapping, Bearer-Header,
Session-Persistenz bei `register`, Session-Wipe bei `deleteAccount`).
### Bekannte Einschränkungen
- `changeEmail`, `changePassword`, `deleteAccount` brauchen Server-
seitig den `bearer`-Plugin von Better Auth oder einen Custom-
Bearer-Resolver. Heute mountet `mana-auth` nur den Cookie-Pfad.
Phase-3-Server-PR im `mana`-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 mit `LocalizedError`-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`.