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>
753 lines
32 KiB
Markdown
753 lines
32 KiB
Markdown
# 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`.
|