η-0: De-Hybrid — WKWebView raus, native Tabs mit Platzhaltern

Lift von Hybrid (WKWebView für Lesen/Erkunden) auf fully-native ist
beschlossen. Diese Phase entfernt die WebShell-Infrastruktur; das volle
native Read-Surface folgt in η-2..η-5 nach docs/NATIVE_LIFT_PLAN.md.

- ManaWebShell-Dep raus aus project.yml
- Sources/Core/WebShell/CookieBridge.swift gelöscht
- RootView auf vier native Tabs (Lesen + Erkunden = Platzhalter,
  Submit + Konto unverändert nativ)
- DocComments in DeepLinkRouter / AppConfig / Account / Settings von
  WebView-Verweisen befreit
- CLAUDE.md Invarianten von Hybrid auf η umgestellt (13 Invarianten,
  pure SwiftUI + Offline-first + SafariView-Ausnahme für Legal)
- PLAN.md auf η-0 + Phasenübersicht η-0..η-10
- AppConfigTests.test_keychainService_matchesSharedGroup auf
  ManaSharedKeychainGroup aktualisiert (war drift seit Cross-App-SSO)

Verifikation:
- xcodebuild iOS-Simulator iPhone 16e: BUILD SUCCEEDED
- nm ZitareNative | grep WKWebView: 0 Referenzen
- otool -L: kein WebKit-Framework-Link
- 20/20 Tests grün

Cross-Repo-Follow-up (η-1 Blocker):
- zitare/apps/zitare/ muss index-full.json + 7 Stammdaten-JSONs liefern
- zitare/apps/api/ Volltext-Search-Endpoint bestätigen/ergänzen

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-05-22 12:32:05 +02:00
parent 58eb2807c7
commit 1d770123f5
11 changed files with 619 additions and 365 deletions

View file

@ -4,8 +4,9 @@ import ManaCore
/// App-spezifische Konfiguration für Zitare. Implementiert
/// `ManaAppConfig` aus ManaCore und ergänzt die Zitare-eigene
/// `apiBaseURL` (api.zitare.com, getrennt von mana-auth) sowie
/// `webBaseURL` (zitare.com, für WKWebView und Universal-Links)
/// und `appBaseURL` (app.zitare.com, für eingeloggte Pfade).
/// `webBaseURL` (zitare.com, für Universal-Links + Snapshot-Pull +
/// SafariView auf Legal-Seiten) und `appBaseURL` (app.zitare.com,
/// historisch SPA-Surface η-0+ nicht mehr vom Native-Client geladen).
///
/// Cutover zu .zitare.com-Subdomains am 2026-05-20. zitare.mana.how
/// ist abgeschaltet; zitare-api.mana.how bleibt als Back-Compat-
@ -36,18 +37,19 @@ enum AppConfig {
/// Edit, Admin, Me).
static let appBaseURL = URL(string: "https://app.zitare.com")!
/// Default-URL für den WebView (öffentliches Lese-Surface).
/// Default-Web-URL (öffentliches Lese-Surface, Snapshot, AASA).
/// η-0+: kein WebView mehr, aber bleibt Universal-Link-Domain und
/// SafariView-Ziel für Legal-Seiten.
static let webBaseURL = publicWebURL
/// Endpoint für den Korpus-Snapshot (Phase ζ-2). Heute noch nicht
/// als statische HTTP-Datei publiziert Aufgabe im Web-Repo:
/// `apps/zitare/static/index-min.json` aus dem Snapshot-Job
/// zusätzlich rauskopieren. Bis dahin schlägt der Pull mit 404
/// fehl und `SnapshotSync.tryRefresh()` macht fail-soft no-op.
/// Endpoint für den Korpus-Snapshot (heute Phase ζ-2: `index-min.json`,
/// in η-1 ersetzt durch `index-full.json` + Stammdaten-Collections,
/// siehe `docs/NATIVE_LIFT_PLAN.md`). Bis das im Web-Repo gebaut ist,
/// schlägt der Pull mit 404 fehl und `SnapshotSync.tryRefresh()` macht
/// fail-soft no-op.
static let snapshotURL = webBaseURL.appendingPathComponent("index-min.json")
/// User-Agent-Suffix für WKWebView (ManaWebShell). WKWebView hängt
/// das an seinen Standard-UA an, ersetzt ihn nicht.
// User-Agent-Suffix für URLSession-API-Calls.
#if os(macOS)
static let userAgent = "ZitareNative/0.1 (macOS)"
#else

View file

@ -1,88 +0,0 @@
import Foundation
import ManaCore
import WebKit
/// Reicht den mana-auth-JWT als Cookie an den `WKWebView` weiter, sodass
/// eingeloggte `(app)`-Routen auf `app.zitare.com` ohne zweiten Login
/// erreichbar sind.
///
/// **Phase ζ-1: Skeleton.** Methoden existieren, werden aber heute
/// nicht aufgerufen bevor sie scharfgeschaltet werden, muss der
/// Cookie-SSO-Pfad auf der Web-Seite (`zitare/apps/api/src/auth/`
/// und `apps/zitare/src/lib/auth/token-helper.ts`) gegen einen
/// *echten* mana-auth-Token End-to-End getestet sein (Verifikations-
/// Lücke in `zitare/STATUS.md`).
///
/// **Cross-Domain-Flow (Cutover 2026-05-20):** Der WKWebView lädt
/// `app.zitare.com` (Brand-Domain). Die SvelteKit-App ruft beim
/// Boot Cross-Origin `POST auth.mana.how/api/v1/auth/refresh` mit
/// `credentials: 'include'` und erwartet einen Refresh-Cookie auf
/// `.mana.how`. Der Cookie-Domain-Wert hier (`.mana.how`) ist
/// genau richtig der Browser sendet ihn an das XHR-Ziel
/// (auth.mana.how), unabhängig vom Source-Page-Host. Identisches
/// Pattern wie im Web-Client (`apps/zitare/src/lib/auth.ts`).
///
/// **Cookie-Schema** (gespiegelt zu mana-auth `better-auth.config.ts`):
/// - Name: `mana.access` (JWT) und optional `mana.refresh` (Opaque)
/// - Domain: `.mana.how` (Cookie wird an auth.mana.how-XHR mitgesendet)
/// - Path: `/`
/// - Secure: true, HTTPOnly: false (WebView muss lesen können)
/// - SameSite: **None** mana-auth setzt für Cross-Subdomain-SSO
/// `sameSite: 'none'` (better-auth.config.ts), ohne das wird der
/// Cookie bei Cross-Origin-POST nicht mitgesendet. Foundation
/// `HTTPCookieStringPolicy` hat dafür keinen Konstanten-Wert
/// `cookieAttributesNone` als Roh-String über die initWithProperties-
/// `String`-Variante.
enum CookieBridge {
/// Setzt den `mana.access`-Cookie im geteilten `WKHTTPCookieStore`,
/// wenn der `AuthClient` einen gültigen JWT hält. No-op sonst.
@MainActor
static func installManaAccess(from auth: AuthClient) async {
guard case .signedIn = auth.status, let token = currentAccessToken(from: auth) else {
Log.web.debug("CookieBridge: kein signedIn-Token, no-op")
return
}
guard let cookie = makeAccessCookie(token: token) else {
Log.web.warning("CookieBridge: konnte Cookie-Properties nicht bauen")
return
}
let store = WKWebsiteDataStore.default().httpCookieStore
await store.setCookie(cookie)
Log.web.info("CookieBridge: mana.access für .mana.how gesetzt")
}
/// Entfernt den `mana.access`-Cookie wieder etwa nach Logout.
@MainActor
static func removeManaAccess() async {
let store = WKWebsiteDataStore.default().httpCookieStore
let cookies = await store.allCookies()
for cookie in cookies where cookie.name == "mana.access" {
await store.deleteCookie(cookie)
}
Log.web.info("CookieBridge: mana.access entfernt")
}
private static func currentAccessToken(from auth: AuthClient) -> String? {
// ManaCore hält den JWT im Keychain. In ζ-3 ersetzt durch die
// tatsächliche `auth.currentAccessToken()`-API; heute nur
// Skelett-Hook, damit Cookie-Setup und API in Reichweite sind.
// Linter beruhigen ohne unused warning:
_ = auth
return nil
}
private static func makeAccessCookie(token: String) -> HTTPCookie? {
// SameSite=None: Foundation hat keinen Konstanten-Wert für
// "None", aber HTTPCookie akzeptiert beliebige Strings für
// .sameSitePolicy. Cross-Origin-POST von app.zitare.com
// auth.mana.how braucht None, sonst kein Cookie-Versand.
HTTPCookie(properties: [
.name: "mana.access",
.value: token,
.domain: ".mana.how",
.path: "/",
.secure: true,
.sameSitePolicy: "None"
])
}
}