Zusaetzlicher Anmeldeweg neben Benutzername/Passwort. Der Login-Screen holt
die konfigurierten Provider von GET /api/auth/oidc/providers; ohne Provider
oder gegen einen aelteren Server bleibt der Screen unveraendert.
Die App laeuft als Public Client: Authorization Code Flow mit PKCE in einer
ASWebAuthenticationSession (nie ein WKWebView), ohne Client Secret. Das
ID-Token wird an POST /api/auth/oidc/exchange gegen ein Calendarr-Token
getauscht; die Antwort hat dieselbe Form wie der Passwort-Login, sodass der
bestehende Session-Pfad unveraendert weiterlaeuft.
- OIDCAuthenticator kapselt Discovery, PKCE-Request, Code-Tausch und die
stille Erneuerung. Der AuthState mit dem Refresh-Token liegt in der
Keychain (kSecAttrAccessibleAfterFirstUnlock) und wird beim Logout aus
beiden Zugriffsgruppen entfernt.
- OIDCSessionRefresher erneuert die Sitzung beim Start, wenn das
Calendarr-Token binnen 24 h ablaeuft — ohne Browser, ueber den
Refresh-Token des Providers.
- Die angefragten Scopes kommen vom Server (mobile_scopes); ein Scope, den
der Provider nicht kennt, wuerde die ganze Anfrage scheitern lassen.
- Bei aktiviertem gefuehrten Zugriff weicht AppAuth auf SFSafariViewController
aus, dessen Redirect nur ueber ein registriertes URL-Schema zurueckkaeme.
Da das App-Target seine Info.plist generiert und CFBundleURLTypes dort
nicht setzbar ist, wird der Fall vorab sauber abgelehnt statt endlos auf
einen Callback zu warten, der nicht kommen kann.
- Abbrueche werden mit Fehlerdomaene geprueft (user- und programCanceled) und
still behandelt; alle uebrigen Fehler tragen Domain, Code und OAuth-Fehler
ins DEBUG-Log, statt spurlos zu verschwinden.
project.pbxproj: AppAuth-iOS 1.7.5 als SPM-Abhaengigkeit, nur am App-Target.
OTHER_LDFLAGS bekommt -ObjC, weil OIDAuthState+IOS.m ausschliesslich eine
Kategorie enthaelt und ohne Symbol sonst wegoptimiert werden koennte.
Achtung: mangels macOS/Xcode ist dieser Stand nicht kompiliert. Beim ersten
Oeffnen loest Xcode das Paket auf und legt Package.resolved an; die bitte
mit committen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The snapshot format was an internal detail shared between the app and its own
widget extension. A second Mac app needs the same data, and the way to give it
that is a documented contract rather than a format it reverse-engineers and then
drifts from. Both targets now link CalendarrCore and go through SnapshotStore.
Shared/WidgetData.swift becomes a thin facade. The typealiases and the flat
colour accessors exist so the ~70 existing call sites across the app and the
widget views compile unchanged; they are a migration convenience, not a design.
The snapshot now carries what a reader outside this app actually needs:
coverageStart / coverageEnd, because the published window is ~7 days back and
~42 ahead. Outside it the snapshot holds no information, which is not the same
as holding no events — and only the writer knows where that edge is. Without it
a consumer renders a convincingly empty March and is simply wrong.
isLoggedIn plus a session record, so a reader can say "sign in to Calendarr"
rather than "open Calendarr once". The events are deleted on sign-out, so the
absence of a cache alone cannot tell those two apart.
writerVersion, purely so a mismatch between the two apps is diagnosable.
The coverage constants move to SnapshotCoverage, so the writer and the code that
reconstructs the window for older files can no longer disagree about it.
CalendarStore needs an explicit `import CalendarrCore` because the app target
builds with SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY, which requires the
defining module to be imported directly rather than picked up transitively.
Verified: builds for iOS and Mac Catalyst; CalendarrKit's 11 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two failures here are silent rather than loud, which is why they have gone
unnoticed on iOS and would have been much harder to diagnose on a Mac.
The old `enum Keychain` discarded all four OSStatus results. On Mac Catalyst a
missing `keychain-access-groups` entitlement makes SecItem calls fail with
errSecMissingEntitlement, and because nothing checked, that was indistinguishable
from "no token stored" — the user would be signed out on every launch, with no
error anywhere. KeychainStore now checks every status, distinguishes
errSecItemNotFound from real failures, and sets kSecUseDataProtectionKeychain so
macOS selects the modern entitlement-gated keychain instead of the legacy login
keychain.
Adding the entitlement moves the default access group to the first array entry,
so that entry is deliberately the app's own group: existing tokens keep
resolving with an unqualified query and nobody is signed out. loadToken() then
migrates forward in three steps — shared group, own default group, and the
pre-Keychain UserDefaults copy.
If the entitlement is not provisioned yet, KeychainStore falls back to the
default group rather than throwing. A hard failure would make the app unusable
for everyone whose provisioning lags; the fallback asserts in DEBUG instead, so
a misconfiguration is loud in development and survivable in production.
Separately, logout() left widget-cache.json in the App Group container, so
widgets kept rendering the signed-out user's events indefinitely. That is an
existing iOS bug, and it would have leaked the same data to any other app
reading the container. WidgetStore.clear() now removes both cache files and
reloads the timelines.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Move the bearer token from UserDefaults to the Keychain (accessible after
first unlock), with a one-time migration so existing logins survive.
- Replace NSAllowsArbitraryLoads=YES with NSAllowsLocalNetworking=YES so ATS
still permits cleartext to LAN/self-hosted servers but enforces TLS for
public hosts (no arbitrary cleartext/MITM).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>