mana-swift-core/CHANGELOG.md
Till JS 8cb6fdc945 feat(feedback): FeedbackImage + images im Submit (v2.2.0)
FeedbackSubmission.images (base64, ohne data:-Praefix, 1:1 zum Web- und
Android-Vertrag); FeedbackClient-Payload sendet images:[{mime,b64}].
Additiv. Test deckt Bild-Payload ab.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 19:56:08 +02:00

753 lines
32 KiB
Markdown
Raw Permalink 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).
## [Unreleased]
## [2.2.0] — 2026-06-12
### Hinzugefügt
- **`FeedbackImage` + `FeedbackSubmission.images`** — optionale
Screenshot-Anhänge im Feedback. `b64` ist reines base64 (ohne
`data:`-Präfix), 1:1 zum Web-`FeedbackImage`- und Android-Vertrag. Der
`FeedbackClient`-Payload sendet sie als `images: [{mime, b64}]` (weggelassen,
wenn leer). Additiv — bestehende Aufrufer brauchen keine Änderung; das
Backend nahm `images` schon entgegen (mana-media-Upload, Vorstand-only).
## [2.1.0] — 2026-06-10
### Hinzugefügt
- **`ManaCreditsClient`** (+ `ManaCreditBalance`, `ManaCreditTransaction`) —
liest das ökosystemweite Mana-Guthaben + die letzten Bewegungen von
`credits.mana.how` (mana-auth-JWT), Pendant zum Web-`@mana/mana-credits-client`.
Plattform-Wissen (Pfade `/api/v1/credits/*`), `baseURL` überschreibbar. Wird
von der geteilten `ManaView`/`ManaBalanceRow` (mana-swift-ui ManaCreditsUI)
gerendert — die native „Gesamt-Mana-Anzeige".
- **`AuthClient.updateName(_:)`** — ändert den Anzeigenamen des eingeloggten
Accounts via `PATCH /api/v1/me/profile` (mana-auth, JWT; derselbe Endpoint,
den der Avatar-Baukasten + die Web-Settings nutzen). 1–80 Zeichen, client-
validiert. Der neue Name landet erst beim nächsten JWT-Mint im Token —
Aufrufer aktualisieren ihre Sicht selbst (`getProfile()`). Schließt die
Lücke, dass der Anzeigename (sonst nur bei Registrierung/Web setzbar) nativ
nicht änderbar war.
- **`ManaTheme.kollekta`** — 21. Signatur-Variant (App Kollekta), 1:1 aus dem
Web-SOT `packages/themes/src/index.ts` gespiegelt (Label „Kollekta",
„Lila-Akzent, sammlerisch", `kollekta.mana.how`). Farbwerte kommen aus dem
Generator (`GeneratedThemes.swift`). Damit gilt **21 Apps ↔ 21 Themes**.
### Geändert
- **`frischgruen`-Primary abgedunkelt** (Nutriphis Signatur-Theme): Light
`142 62% 31%` → `142 64% 25%`, Dark `142 60% 52%` → `142 58% 43%`. Quelle ist
`mana/packages/themes/src/variants/frischgruen.css`; `GeneratedThemes.swift`
wurde per Generator nachgezogen. Gilt web + nativ konsistent.
## [2.1.0] — 2026-06-03
Minor — **`AuthenticatedTransport.request` akzeptiert jetzt einen
optionalen `timeout`.** Additiv, abwärtskompatibel (Default `nil` =
URLSession-60-s-Default unverändert). Für langsame LLM-/Job-Endpoints
höher setzen, sonst bricht der Client ab, während der Server noch
rechnet — genau das passierte beim KI-Drehbuch in audioguide-native
(synchroner LLM-Call > 60 s → „Zeitüberschreitung", obwohl der Server
das Drehbuch danach speicherte). Consumer müssen nichts anpassen.
## [2.0.0] — 2026-06-03
Major — **Theme-Pass-Vereinheitlichung: 20 Apps ↔ 20 Themes, keine
Baseline-Variants mehr.** Jede Variant ist jetzt die Signatur genau einer
App; das „immer freie Baseline"-Konzept ist abgeschafft.
Breaking an `ManaTheme`:
- **Entfernt:** `ManaTheme.forest`, `.neutral`, `.monochrome` (waren die
verwaisten Baselines ohne Signatur-App). Code, der diese Cases
referenziert, bricht — auf eine andere Signatur-Variant umstellen.
- **Neu:** `ManaTheme.schaenke` — Signatur von **Who** („Schänke",
Kerzenlicht/Terrakotta-Rot, WCAG-AA light+dark).
- **`mana` ist jetzt Hubs Signatur** (`signatureApp == "hub"`,
freischaltbar durchs Hub-Ausprobieren) statt Baseline.
- **`signatureApp`/`signatureAppName` sind jetzt non-optional** (`String`
statt `String?`) — jede Variant hat eine. `isUnlocked(triedApps:)`
prüft schlicht `triedApps.contains(signatureApp)`.
- **`isBaseline` ist `@deprecated` und immer `false`.** Bleibt vorerst aus
Kompatibilität, sollte aus Consumer-Code raus.
`GeneratedThemes.swift` (autogeneriert) führt jetzt 20 statt 22 Statics.
Spiegelt `@mana/themes@0.7.0` (web). Apps müssen rebuilden; exhaustive
`switch`-Statements über `ManaTheme` ggf. um `.schaenke` ergänzen.
## [1.16.0] — 2026-06-02
Minor — **Shared-Keychain-Logout-Kaskade gefixt (Flotten-Auth-Härtung).**
Alle 16 nativen mana-Apps teilen sich EINEN Keychain (`ev.mana.session`,
Service + Access-Group). Damit ist ein `keychain.wipe()` ein flotten-weiter
Logout — eine App, die einen einzelnen 401 fängt, loggt alle aus. Drei
zusammenhängende Ursachen behoben:
- **Default `RefreshFailurePolicy` ist jetzt `.softFirst`** (vorher
`.immediateWipe`). 13 von 16 Apps liefen auf dem gefährlichen Default.
Apps mit isoliertem Keychain können `.immediateWipe` explizit setzen.
- **`softFirst` korrigiert.** Wipte früher schon, sobald in diesem Prozess
**einmal** ein Refresh erfolgreich war (`refreshOnceSucceeded`). Da der
`scenePhase`-Heartbeat beim App-Start sofort einen erfolgreichen Refresh
macht, verhielt sich `softFirst` praktisch wie `immediateWipe` — der
eigentliche Logout-Bug. Jetzt zählt rein der Failure-Count: erst der
**zweite invalidierende Fehler in Folge** wiped.
- **Re-Read vor Wipe.** `AuthClient.performRefresh()` liest vor jedem Wipe
den Keychain neu. Hat ein anderer Prozess (andere App, Widget)
zwischenzeitlich einen frischen Token geschrieben, war der 401 ein
Stale-Token-Artefakt → einmal mit dem neuen Token wiederholen statt die
Flotte auszuloggen. Selbst `.immediateWipe` wiped in diesem Fall nicht.
- **Transport refresht nicht mehr blind bei jedem 401.** Lebt der gerade
von `freshAccessToken()` gelieferte Token noch >60 s, ist ein 401 kein
Token-Ablauf, sondern ein Authz-/Backend-401 (z. B. hub-api-Proxy-Bridge)
→ wird durchgereicht statt einen Refresh (und damit einen möglichen Wipe)
zu erzwingen.
- **`TokenResponse.refreshToken` ist optional.** Der Server leitet ihn aus
einem best-effort `get-session`-Subcall ab, der transient fehlschlagen
kann → Feld fehlt im 200-Response. Früher: DecodingError auf einem
erfolgreichen Refresh → Access-Token nie gespeichert → Retry-Loop. Jetzt:
fehlt/leer → bestehenden (stabilen, 90 d) Session-Token behalten.
Keine API-Signatur-Änderungen. Apps mit `path:`-Dep bekommen den Fix beim
nächsten Rebuild; Apps auf `from:` brauchen den Bump auf `1.16.0`.
Tests: 4 neue/aktualisierte in `AuthClientGuestAndResilienceTests`.
## [1.15.0] — 2026-06-02
Minor — **Theme-Pass: Signatur-App-Präsentationsdaten an `ManaTheme`.**
- `ManaTheme.signatureAppName: String?` — Anzeige-Name der Signatur-App
(z. B. `skylight` → „Wordeck"), `nil` für Baseline.
- `ManaTheme.signatureAppHomepage: URL?` — eigene Landingpage der Signatur-App
(Apex-Domain wie `wordeck.com`, Fallback `<slug>.mana.how`), `nil` für Baseline.
- Additiv. Treibt den neuen Locked-Hinweis + „<App> öffnen"-Button in
`ManaThemeGallery` (mana-swift-ui ≥ 0.10.0): gesperrte Variant öffnet die App
nicht mehr automatisch, sondern zeigt Text + Button zur eigenen Landingpage.
Siehe THEME_PASS.md.
## [1.14.0] — 2026-06-01
Minor — **Theme-Pass: `ThemePass` (Unlock-Ledger-Client) + `ManaAppConfig.appSlug`.**
- `ThemePass` (actor, ManaCore): verwaltet den Set ausprobierter App-Slugs.
**Keychain-Schicht** (geteilter Service `ev.mana.themepass` + Shared-Group →
cross-app on-device, offline/anonym) **+ Server-Schicht** (mana-auth
`/api/v1/settings` + `POST /visited/:app`, cross-device). `recordVisit(app:)`,
`sync()`, `localTriedApps()`.
- `ManaAppConfig.appSlug` (optional, Default nil) + `KeychainStore.Key.triedApps`.
- Clients leiten freigeschaltete Variants via `ManaTheme.isUnlocked(triedApps:)`
(1.13.0) ab + füttern `ManaThemeGallery(unlocked:)`. Foundation-only.
Braucht den mana-auth-Deploy (triedApps-Migration). Siehe THEME_PASS.md.
## [1.13.0] — 2026-06-01
Minor — **12 weitere App-Signatur-Variants (Theme-Pass Phase 1 komplett).**
Library 10 → **22**. Pro App eine eigene Signatur:
- `buchseite` (Zitare), `herbar` (Herbatrium), `frischgruen` (Nutriphi),
`wegspur` (Viadocu), `waldlichtung` (Manawald), `panel` (Comicello),
`buehne` (Mukke), `kreisel` (Kreisel), `garderobe` (Werdrobe),
`wolke` (Uload), `hoerweg` (Audioguide), `spektrum` (ManaMeme).
Je `ManaTheme`-Case + DE-`displayName` + `characterDescription` +
generierte `ManaThemeColors`. Alle WCAG-AA + 12-Token grün (zwei Primaries
abgedunkelt fürs Weiß-auf-Akzent-Gate). **Erste Pässe** — Feinschliff mit
den App-Ownern offen. Damit hat jede mana-App ihr eigenes Theme; alle in
jeder Galerie wählbar. Nächster Theme-Pass-Schritt: Unlock-Ledger.
- Unlock-Metadaten: `ManaTheme.isBaseline`, `.signatureApp` (App-Slug der
Freischalt-Quelle), `.isUnlocked(triedApps:)` — Basis für den Theme-Pass-Ledger.
## [1.12.0] — 2026-06-01
Minor — **App-Signatur-Variants (Theme-Pass Phase 1).** Library 8 → 10:
- **`bodensee`** (Seepuls) — „Bodensee bei Nacht", Cyan-Wasser +
Bernstein; dunkler Modus byte-nah aus seepuls-native, hell neu.
- **`pinnwand`** (Wunsch) — Beige + Orange, einladend; hell aus dem
Wunsch-Charakter, dunkler Modus neu (warm-braun).
Je `ManaTheme`-Case + `displayName` + `characterDescription` +
`ManaThemeColors` (generiert). Quelle: `mana/packages/themes/src/
variants/{bodensee,pinnwand}.css` → `gen:swift`. WCAG-AA + 12-Token-Gates
grün. Weitere ~11 Signaturen + Unlock-Ledger offen (THEME_PASS.md).
## [1.11.0] — 2026-05-31
Minor — **Theme-Präsentations-Metadaten (additiv)**. `ManaTheme` liefert
jetzt pro Variant einen deutschen Anzeige-Namen und einen einzeiligen
Charakter-Text — damit Apps einen Theme-Picker bauen können, ohne die
Labels selbst zu pflegen. Spiegelt die Variant-Tabelle in
`mana/docs/THEMING.md`.
- Neu `ManaTheme.displayName` — kurzer DE-Name (z.B. `.paper` → „Papier",
`.forest` → „Wald").
- Neu `ManaTheme.characterDescription` — einzeiliger Charakter für
Picker-Untertitel (z.B. „Sepia, warm, lesefokussiert").
- Rein additiv, keine Verhaltensänderung. Untermauert die
Customization-Freiheit-Doktrin (THEMING.md, 2026-05-31): jede App
unterstützt alle Variants. Referenz-Konsument: `pageta-native`
Theme-Galerie.
## [1.10.0] — 2026-05-29
Minor — **Marken-Schriften nativ (additiv)**. `ManaTokens` bündelt jetzt
die selbst-gehosteten Variable-Fonts des Vereins — **Inter** (sans),
**Source Serif 4** (serif), **JetBrains Mono** (mono), OFL, derselbe
Schriftsatz wie im Web (`@mana/themes`), kein CDN.
- Neu `ManaFonts` — registriert die gebündelten TTFs idempotent bei
CoreText; Familien-Namen matchen die Web-`@font-face` (per Test verifiziert).
- Neu `ManaBrandTypography` — spiegelt die `ManaTypography`-API, aber in
den Marken-Schriften (Titel in der Serife, Body in Inter, `code` in
JetBrains Mono), via `relativeTo:` an Dynamic Type gekoppelt.
- **Nichts ändert sich automatisch:** `ManaTypography` (System-SF) bleibt
unangetastet. Eine App adoptiert das Marken-Gesicht durch Wechsel auf
`ManaBrandTypography` — opt-in.
- **Hinweis Binärgröße:** Die Fonts (~4 MB) liegen in `ManaTokens` und
werden damit von jedem Konsumenten mitgebündelt. Wird `ManaTokens` zu
schwer, ist ein eigenes opt-in-Product `ManaFonts` der nächste Schritt
(berührt die Zwei-Produkte-Invariante → Diskussion mit Till).
## [1.9.1] — 2026-05-29
Patch — **Refresh-Coalescing gegen Logout-Race**. Parallele
`refreshAccessToken()`-Aufrufe (z.B. zwei gleichzeitige
authentifizierte Requests plus der scenePhase-Heartbeat einer App)
feuerten bislang jeweils einen eigenen `/refresh`. Mit
Refresh-Token-Rotation server-seitig schickte der zweite Aufruf den
bereits rotierten Token → 401 „session invalid" → bei
`.immediateWipe` (Default) sofortiger Logout. Symptom in
viadocu-native: Foto-Import im Settings-Tab (lädt Stats parallel)
loggte den User aus.
### Behoben
- `refreshAccessToken()` koalesziert jetzt: ein laufender Refresh wird
von allen parallelen Aufrufern geteilt (`inFlightRefresh`-Task),
statt mehrere `/refresh`-Calls mit demselben Token abzufeuern. Die
eigentliche Logik liegt im neuen privaten `performRefresh()`.
### Hinweis
- Kein API-Change. Apps müssen nichts anpassen. `softFirst` hilft
gegen diese Race **nicht** (nach einem erfolgreichen Refresh ist
`refreshOnceSucceeded == true` → wiped trotzdem) — das Coalescing
ist der korrekte Fix.
## [1.9.0] — 2026-05-28
Minor — **`ManaFeedback`-Kit**: nativer Client für den zentralen
Feedback-Eingang der Wunsch-App (`wunsch.mana.how/api/feedback`),
Pendant zum Web-`@mana/shared-feedback`.
### Hinzugefügt
- `FeedbackKind` (`wunsch`/`bug`/`feedback`), `FeedbackPlatform`
(`.current` → ios/macos), `FeedbackSubmission`, `FeedbackResult`,
`FeedbackError`.
- `FeedbackClient(appId:endpoint:session:tokenProvider:)` mit
`submit(_:)` — POSTet den Web-Vertrag (inkl. Honeypot `_hp`) und
ergänzt Auto-Kontext (App-Version, OS, Locale, Gerät). `tokenProvider`
liefert den Bearer zum Sende-Zeitpunkt; `nil`/leer → anonym.
Foundation-only (Invariante 2 gewahrt), kein neues Product (Teil von
`ManaCore`). Kein Breaking-Change. Siehe `mana/docs/FEEDBACK_NATIVE_PLAN.md`.
## [1.8.0] — 2026-05-21
Minor — **`RefreshFailurePolicy`** + Diagnostik-Logging in
`refreshAccessToken()`.
### Hintergrund
Eine mana-auth-Regression am 2026-05-19 (`project_auth_refresh_bug`)
hat ~80% aller `/refresh`-Calls 401en lassen — ManaCore hat daraufhin
**sofort** den Keychain gewiped und alle Apps in den Login-Screen
geworfen. Server-Bug war binnen Stunden gefixt, aber für User mit
einem Cold-Launch in genau diesem Fenster war's "logged out".
Außerdem fragte Till mehrfach "warum muss ich mich nach jedem TestFlight-
Update neu einloggen" — Hypothese: transienter Server-/Deployment-Glitch
beim ersten Refresh nach Update löst Wipe aus, obwohl die Session
eigentlich gültig ist.
### Neu
- **`RefreshFailurePolicy`** (public enum, Sendable):
- `.immediateWipe` — Default. Bestehendes Verhalten: jeder Server-
"Session-tot" → sofort Keychain wipe → `.signedOut`.
- `.softFirst` — Erster Refresh-Fehler im Prozess wird nicht gewiped,
Session bleibt erhalten, Fehler wird geworfen. Wipe erst beim
zweiten Fehler ODER nach einem zuvor erfolgreichen Refresh im
selben Prozess (dann ist der invalidate-Response vertrauenswürdig).
- **`ManaAppConfig.refreshFailurePolicy`** (default `.immediateWipe`,
Protocol-Extension). Apps können opt-in per `DefaultManaAppConfig`-
Init-Parameter oder eigener Adopter-Implementierung.
- **`AuthClient.refreshOnceSucceeded`** + **`refreshFailureCount`**
(private(set)) — public-readable für Diagnose + Tests.
### Geändert
- `refreshAccessToken()` loggt jetzt jeden Attempt inklusive Token-
Länge, once-succeeded-Flag, Failure-Count und gewählter Policy.
Failure-Pfad loggt zusätzlich HTTP-Status und Response-Body-Excerpt
(erste 256 Zeichen). Logs gehen weiter über `CoreLog.auth`, also
`log show --predicate 'subsystem == "ev.mana.core"'` fängt sie.
- Erfolgreicher Refresh resettet `refreshFailureCount` auf 0.
- Nichts breaking. Default-Verhalten für alle bestehenden Apps
unverändert.
### Tests
- 4 neue Tests in `AuthClientGuestAndResilienceTests`:
- `softFirst`: erster 401 behält Session, zweiter wiped
- `softFirst`: 401 nach erfolgreichem Refresh wiped sofort
- `softFirst`: transienter 503 zählt nicht in Failure-Count
- `immediateWipe`: erster 401 wiped (Default unverändert)
- 89/89 grün (vorher 85/85).
### Adoption
Apps die opt-in wollen:
```swift
static let manaAppConfig: ManaAppConfig = DefaultManaAppConfig(
authBaseURL: URL(string: "https://auth.mana.how")!,
keychainService: ManaSharedKeychainGroup,
keychainAccessGroup: ManaSharedKeychainGroup,
appGroup: "group.ev.mana.<app>",
refreshFailurePolicy: .softFirst
)
```
Pageta-native ist der erste Konsument. Andere SSO-Apps können später
nachziehen, falls Till oder Logs es nahelegen.
## [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`.