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>
32 KiB
Changelog
Alle Änderungen werden hier dokumentiert. Format orientiert an Keep a Changelog, Versionierung nach Semver.
[Unreleased]
[2.2.0] — 2026-06-12
Hinzugefügt
FeedbackImage+FeedbackSubmission.images— optionale Screenshot-Anhänge im Feedback.b64ist reines base64 (ohnedata:-Präfix), 1:1 zum Web-FeedbackImage- und Android-Vertrag. DerFeedbackClient-Payload sendet sie alsimages: [{mime, b64}](weggelassen, wenn leer). Additiv — bestehende Aufrufer brauchen keine Änderung; das Backend nahmimagesschon 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 voncredits.mana.how(mana-auth-JWT), Pendant zum Web-@mana/mana-credits-client. Plattform-Wissen (Pfade/api/v1/credits/*),baseURLüberschreibbar. Wird von der geteiltenManaView/ManaBalanceRow(mana-swift-ui ManaCreditsUI) gerendert — die native „Gesamt-Mana-Anzeige".AuthClient.updateName(_:)— ändert den Anzeigenamen des eingeloggten Accounts viaPATCH /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-SOTpackages/themes/src/index.tsgespiegelt (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): Light142 62% 31%→142 64% 25%, Dark142 60% 52%→142 58% 43%. Quelle istmana/packages/themes/src/variants/frischgruen.css;GeneratedThemes.swiftwurde 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). manaist jetzt Hubs Signatur (signatureApp == "hub", freischaltbar durchs Hub-Ausprobieren) statt Baseline.signatureApp/signatureAppNamesind jetzt non-optional (StringstattString?) — jede Variant hat eine.isUnlocked(triedApps:)prüft schlichttriedApps.contains(signatureApp).isBaselineist@deprecatedund immerfalse. 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
RefreshFailurePolicyist jetzt.softFirst(vorher.immediateWipe). 13 von 16 Apps liefen auf dem gefährlichen Default. Apps mit isoliertem Keychain können.immediateWipeexplizit setzen. softFirstkorrigiert. Wipte früher schon, sobald in diesem Prozess einmal ein Refresh erfolgreich war (refreshOnceSucceeded). Da derscenePhase-Heartbeat beim App-Start sofort einen erfolgreichen Refresh macht, verhielt sichsoftFirstpraktisch wieimmediateWipe— 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.immediateWipewiped 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.refreshTokenist optional. Der Server leitet ihn aus einem best-effortget-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"),nilfür Baseline.ManaTheme.signatureAppHomepage: URL?— eigene Landingpage der Signatur-App (Apex-Domain wiewordeck.com, Fallback<slug>.mana.how),nilfür Baseline.- Additiv. Treibt den neuen Locked-Hinweis + „ ö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 Serviceev.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ütternManaThemeGallery(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-nativeTheme-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 dieManaTypography-API, aber in den Marken-Schriften (Titel in der Serife, Body in Inter,codein JetBrains Mono), viarelativeTo:an Dynamic Type gekoppelt. - Nichts ändert sich automatisch:
ManaTypography(System-SF) bleibt unangetastet. Eine App adoptiert das Marken-Gesicht durch Wechsel aufManaBrandTypography— opt-in. - Hinweis Binärgröße: Die Fonts (~4 MB) liegen in
ManaTokensund werden damit von jedem Konsumenten mitgebündelt. WirdManaTokenszu schwer, ist ein eigenes opt-in-ProductManaFontsder 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 privatenperformRefresh().
Hinweis
- Kein API-Change. Apps müssen nichts anpassen.
softFirsthilft gegen diese Race nicht (nach einem erfolgreichen Refresh istrefreshOnceSucceeded == 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:)mitsubmit(_:)— POSTet den Web-Vertrag (inkl. Honeypot_hp) und ergänzt Auto-Kontext (App-Version, OS, Locale, Gerät).tokenProviderliefert 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 perDefaultManaAppConfig- 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 überCoreLog.auth, alsolog show --predicate 'subsystem == "ev.mana.core"'fängt sie.- Erfolgreicher Refresh resettet
refreshFailureCountauf 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 wipedsoftFirst: 401 nach erfolgreichem Refresh wiped sofortsoftFirst: transienter 503 zählt nicht in Failure-CountimmediateWipe: erster 401 wiped (Default unverändert)
- 89/89 grün (vorher 85/85).
Adoption
Apps die opt-in wollen:
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 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.