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

32 KiB
Raw Permalink Blame History

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. 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 + „ ö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:

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:

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:

// 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.