Mini-Sprint A des 2FA-Vollausbaus. Apps mit aktivem TOTP-2FA können
sich nativ einloggen. Komplett additiv.
AuthClient.Status um .twoFactorRequired(token, methods, email)
erweitert. signIn() erkennt automatisch den Server-Pfad
{twoFactorRequired: true, ...} und routet zum neuen Status.
Neue Methoden in AuthClient+Account:
- verifyTotp(code:trustDevice:) — 6-stellige Codes aus Authenticator-
App. Bei Erfolg .signedIn, bei Fehler bleibt Status im Challenge
(User kann retry mit anderem Code).
- verifyBackupCode(code:trustDevice:) — einmalige Codes als Fallback.
Wire-Format: Client schickt {code, twoFactorToken, trustDevice} an
/api/v1/auth/two-factor/verify-{totp,backup-code}. Server (mana-auth)
re-injectet den twoFactorToken als better-auth.two_factor-Cookie und
delegiert an Better Auths Plugin.
5 neue Tests, 59/59 grün.
Setzt mana-auth-Server mit den entsprechenden Custom-Endpoints
voraus — siehe gleichzeitiger Commit im mana-Repo.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
250 lines
10 KiB
Markdown
250 lines
10 KiB
Markdown
# Changelog
|
||
|
||
Alle Änderungen werden hier dokumentiert. Format orientiert an
|
||
[Keep a Changelog](https://keepachangelog.com), Versionierung nach
|
||
[Semver](https://semver.org).
|
||
|
||
## [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`.
|