rebreak-monorepo/apps/rebreak-native/tmp/webcontent-layer2-research.md
chahinebrini b31066a04c feat(chat): native action sheet + Insta-style heart for DM messages
- ChatBubble: useActionSheet replaces custom Modal (native iOS popup, Android bottom sheet)
- DM mode (isDM prop): hides like-count, shows Insta-style heart badge under bubble when liked
- Group chat unchanged
- Cleanup: remove unused Modal/Platform imports, sheet styles, actionsOpen state
- deploy.sh: auto-detect ANDROID_HOME + auto-create local.properties for local Gradle
- NEXT_RELEASE.md: DM reactions release note
- Includes other staged work across binder-mac, marketing, ops/mdm, ios/
2026-05-30 09:14:32 +02:00

184 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Layer 2 — ManagedSettings `webContent`-Filter als Always-On-Fallback
**Recherche + Bewertung — iOS 26, rebreak-native. Stand: 2026-05-21. KEINE Code-Änderungen.**
---
## TL;DR / Empfehlung
Die Idee in der vorgeschlagenen Form (**statische Top-50-Gambling-Domain-Liste**, länderabhängig, „Always-On-Fallback wenn NEURLFilter aus") ist **technisch machbar, aber strategisch schwach**. Drei Kernbefunde:
1. **Es gibt KEINE Gambling-`WebDomainCategory`.** `WebContentSettings.FilterPolicy` kennt nur `.none / .specific / .auto / .all`. `.auto` blockt ausschließlich **Adult Content** (Apple-Wortlaut). Eine Gambling-Kategorie existiert nirgends — also bleibt nur die manuelle 50er-Liste.
2. **Der „50-Domain-Cap" ist real und Apple-dokumentiert** (nicht Projekt-Hypothese): *„Your app can block up to 50 web domains and specify up to 50 web domains exceptions at once."* Steht wörtlich in der `blockedByFilter`-Doc und bei `.specific(_:)` / `.auto(_:except:)`.
3. **Der „Fallback"-Nutzen ist fragwürdig**, weil Layer 1 und Layer 2 dieselbe einzelne Schwachstelle teilen: **wenn der User die Family-Controls-Authorization widerruft, fallen NICHT nur ManagedSettings, sondern faktisch der gesamte Tamper-Schutz weg.** Der „NEURLFilter-off"-Fall, gegen den Layer 2 absichern soll, ist nur eine Teilmenge des größeren Lochs.
**Empfehlung:** Layer 2 **nicht als 50er-Domain-Always-On-Fallback** bauen. Stattdessen — falls überhaupt — als **schmales Defense-in-Depth-Add-on**: die Top-~50 Gambling-Domains des Nutzerlandes via `webContent.blockedByFilter = .specific(...)` setzen, **erklärt als „Extra-Härtung", nicht als vollwertiger Fallback**. Der reale Gewinn ist gering; der ehrliche Rat ist, die Energie eher in **Bypass-Detection + Re-Aktivierungs-Nudges** (existiert schon ansatzweise: `recoveringFromBypass`, `/api/protection/state`) zu stecken. Details + Entscheidungsfragen unten.
---
## 1. `ManagedSettings.WebContentSettings`-API — verifizierte Fakten
### `WebContentSettings`
- Struct, conformt `ManagedSettingsGroup`, verfügbar **iOS 15.0+**. Zugriff via `ManagedSettingsStore().webContent`.
- Relevante Property:
```swift
var blockedByFilter: WebContentSettings.FilterPolicy?
```
*„The current policy for filtering websites."* Default `nil` (kein Effekt).
### `WebContentSettings.FilterPolicy` — alle vier Cases (Apple-Doc, verifiziert)
```swift
case none // kein Effekt
case specific(Set<WebDomain>) // blockt genau diese Domains
case auto(Set<WebDomain> = [], except: Set<WebDomain> = [])
// System blockt ADULT CONTENT
// (+ optional zusätzliche Domains, - Ausnahmen)
case all(except: Set<WebDomain>) // blockt ALLES außer Ausnahmen (Allowlist-Modus)
```
### `WebDomain`
Token-Typ, der eine Domain repräsentiert (Initialisierung typ. mit `WebDomain(domain: "bet365.com")`). `Set<WebDomain>` ist das Argument bei `.specific`/`.auto`.
### Gibt es eine Gambling-`WebDomainCategory`? — NEIN.
- `FilterPolicy` hat **keinen kategoriebasierten Case**. Nur Adult-Content (`.auto`) ist kategorieähnlich, und das ist hardcoded auf Adult — **nicht** Gambling, **nicht** erweiterbar.
- `WebDomainCategory` / `webDomainCategories` existiert — aber gehört zu **`ShieldSettings`**, nicht zum `webContent`-Filter. Und (Apple-Doc wörtlich): *Shielding ist eine UI-Overlay-Funktion* — *„the system calls your extension that customizes the shield's appearance"*. **Shielding blockt keinen Traffic**, es zeigt ein Overlay über bereits-erlaubten Apps/Domains. Für tatsächliches Web-Blocking irrelevant.
- **Fazit Punkt 1 der Aufgabe:** Eine „Gambling-Kategorie statt 50er-Liste" gibt es auf iOS schlicht nicht. Die 50er-Liste ist der einzige Weg über `webContent`.
---
## 2. Der „50-Domain-Cap" — VERIFIZIERT, hart, Apple-dokumentiert
Frühere Projektannahme war korrekt. Apple-Doc-Wortlaut (`blockedByFilter`, `.specific(_:)`, `.auto(_:except:)`):
> **„Your app can block up to 50 web domains and specify up to 50 web domains exceptions at once."**
- Gilt für `.specific` **und** `.auto`. Harte Obergrenze, keine Konfigurations-Option.
- Damit ist eine 208k-Domain-Liste (wie bei NEURLFilter/PIR) über `webContent` **prinzipiell unmöglich**. Layer 2 ist API-bedingt auf eine **kuratierte Top-50** beschränkt.
- (Abzugrenzen von der separaten WebKit-Content-Blocker-Grenze von 50.000 *Rules* — das ist eine andere API und nicht gemeint.)
---
## 3. Wirkungsbereich — Safari sicher; restliche WebKit-Browser unbelegt
- **Belegt:** `webContent.blockedByFilter` wirkt auf **Safari** — Apple erwähnt explizit den Nebeneffekt *„Setting any filter policy besides `.none` will disable Safari private browsing."*
- Das ist ein **systemweiter Screen-Time-Mechanismus** (ManagedSettings = der „Enforcer" hinter Screen Time), kein App-lokaler Filter. Drittanbieter-Browser, die WebKit nutzen (auf iOS müssen das de facto alle), greifen mit hoher Wahrscheinlichkeit auf dieselbe Web-Content-Restriction zu — das ist auch das beobachtbare Screen-Time-Verhalten.
- **Hypothese, ungeprüft:** Dass `blockedByFilter` *auch* in Chrome/Firefox/Drittanbieter-WKWebViews greift, ist plausibel (Screen-Time-Webcontent-Restriction wirkt klassischerweise browserübergreifend), aber **nicht per Apple-Doc-Zitat belegt**. Apple dokumentiert nur Safari namentlich. Vor Produktiv-Versprechen muss das auf dem iPhone-Build empirisch getestet werden (Chrome iOS → bet365 öffnen).
---
## 4. Family-Controls-Voraussetzung — ja, FC reicht; aber genau das ist die Crux
- `ManagedSettingsStore`-Restriktionen wirken **nur**, wenn die App eine gültige Family-Controls-Authorization hat (`AuthorizationCenter.shared.requestAuthorization(for: .individual)` → `.approved`). Ohne Authorization sind ManagedSettings-Settings stumm.
- **Kein MDM nötig** — `.individual`-Authorization genügt. Das deckt sich exakt mit dem schon im Repo gebauten `activateFamilyControls`-Pfad (`RebreakProtectionModule.swift`, Z. 246296: FC-Auth → `ManagedSettingsStore(...).application.denyAppRemoval = true`).
- **Lokaler Xcode-Dev-Build:** funktioniert mit dem Development-FC-Entitlement (Repo nutzt `REBREAK_ENABLE_FAMILY_CONTROLS=1` im Plugin, Z. 65). v0.3.4 hat zusätzlich das Distribution-Entitlement (genehmigt) → auch TestFlight/Store ok.
- **Wichtige verifizierte Schwäche:** *„All ManagedSettingsStore restrictions are lifted immediately by the system when authorization is revoked, and the app receives no notification."* Der User kann FC in `Einstellungen → Bildschirmzeit` widerrufen — dann ist Layer 2 **lautlos weg**, ohne Callback. Das untergräbt die „Always-On"-Behauptung.
---
## 5. Koexistenz NEURLFilter + ManagedSettings — unkritisch
- Zwei **getrennte Subsysteme**: NEURLFilter = NetworkExtension (Netzwerk-Pfad), `webContent` = ManagedSettings/Screen-Time (WebKit-Restriction). Sie laufen auf verschiedenen Ebenen, kein gemeinsamer State, keine dokumentierte Konflikt-Konstellation.
- Effektiv ein logisches **OR**: eine Domain wird geblockt, wenn *einer* der beiden Layer sie fängt. Keine Reihenfolge-Abhängigkeit.
- Beide brauchen ohnehin schon Entitlements, die die App hat (`url-filter-provider` + `family-controls`, siehe `with-rebreak-protection-ios.js` Z. 6067). Layer 2 fügt **kein neues Entitlement** hinzu.
- **Bewertung:** Koexistenz ist der unproblematischste Punkt. Technisch sauber kombinierbar.
---
## 6. Kann der User NEURLFilter „offtogglen"? — Die Kernfrage, ehrlich beantwortet
Das ist die Annahme, auf der „Fallback" steht. Nüchterner Befund:
- **`NEURLFilterManager`:** `isEnabled` (Bool, App-gesteuert) und `shouldFailClosed` (Bool — bei `true` wird Traffic geblockt, wenn der Filter nicht erreichbar ist; das Repo setzt `shouldFailClosed = true`, `RebreakProtectionModule.swift` Z. 110). Beides setzt **die App**, nicht der User.
- **System-Toggle für den User?** NetworkExtension-Filter erscheinen üblicherweise unter `Einstellungen → Allgemein → VPN & Geräteverwaltung` bzw. als Filter-Eintrag, den der User abschalten/löschen kann. Genau dieses Verhalten ist im Repo-Code bereits sichtbar: `resetUrlFilter` existiert nur, weil der User „Nicht erlauben" tippen kann und iOS den Denied-State cached (`protection.ts` Z. 146160). **Hypothese, gut gestützt, aber nicht per Apple-Doc-Zitat zu iOS 26 final belegt:** Ja, der User kann NEURLFilter abschalten/ablehnen — entweder beim System-Permission-Dialog oder nachträglich in den Einstellungen.
- **ABER — das ist der Punkt:** Wenn der User NEURLFilter abschaltet, ohne FC anzufassen, **fängt Layer 2 das tatsächlich ab** → das ist der einzige saubere Gewinn-Fall.
- **Das größere Loch:** Wenn der User stattdessen in `Bildschirmzeit` die **FC-Authorization widerruft** (oder Bildschirmzeit ganz deaktiviert), fallen `denyAppRemoval` **und** Layer 2 gleichzeitig weg — lautlos, ohne App-Callback (siehe §4). Layer 2 schützt also **nicht** gegen den motiviertesten Bypass-Pfad eines spielsuchtgetriebenen Nutzers, sondern nur gegen die *halbherzige* Variante „NEURLFilter aus, FC vergessen".
- **Fazit:** Das Fallback-Szenario *kann* real eintreten — aber es ist der schwächere von zwei Bypass-Pfaden. Layer 2 deckt das kleinere Loch und lässt das größere offen.
---
## 7. Länderabhängigkeit — sinnvoll, aber simpel halten
- **Warum überhaupt pro Land?** Gambling-Märkte sind national stark segmentiert (DE: Tipico, bwin; UK: bet365, William Hill, Sky Bet; FR: Winamax, PMU/Betclic; etc.). Eine globale 50er-Liste verschwendet Slots an Domains, die im Land des Nutzers irrelevant sind. Bei nur **50 Slots** ist Kuratierung pro Land der Hebel, der den Cap erträglich macht.
- **Landbestimmung — Optionen, geordnet nach Eignung:**
1. **Device-Region** (`Locale.current.region` / `NSLocale.countryCode`) — lokal, kein Netz, datensparsam. Empfehlung. Schwäche: Region ≠ Aufenthaltsort.
2. **User-Profil** — das Repo hat bereits DiGA-Demographie (MEMORY: `birth_year/profession/...` user-initiiert). Ein optionales „Land"-Feld wäre DSGVO-konform und am genauesten. Aber: zusätzliche UX, und Demographie ist strikt user-initiated.
3. **IP-Geo** — am genausten für „wo bin ich", aber Netzabhängig + datenschutzkritisch (Glücksspiel-Stigma, DiGA). **Nicht empfohlen.**
- **Empfehlung:** Device-Region als Default, optionales Profil-Override. Kein IP-Geo.
- **Datenquelle der Liste:** Da der Inhalt sich selten ändert, eignet sich eine **statische, mit der App gebundelte JSON** (`country → [top50 domains]`) — kein Backend-Roundtrip, funktioniert offline, kein neuer Endpoint. Alternative: bestehender Backend-Endpoint `/api/url-filter/...` um ein `top50?country=DE`-Feld erweitern, falls schnellere Updates ohne App-Release gewünscht sind. Für ~50 stabile Domains ist das Backend-Overkill.
---
## Bewertung — ehrlich, auch kritisch
### Mehrwert für Rebreak
- **Was Layer 2 abfängt, das Layer 1 nicht abdeckt:** ausschließlich den Fall „User hat NEURLFilter abgeschaltet/abgelehnt, FC aber noch aktiv". In diesem (und nur diesem) Fenster blockt `webContent` weiterhin die Top-50.
- **Ist das der echte Gewinn?** Eher **marginal**. Begründung:
- Es deckt nur **50 von 208.000** Domains ab — ein spielsuchtgetriebener Nutzer findet trivial eine Casino-Domain außerhalb der Top-50.
- Es schützt **nicht** gegen den motiviertesten Bypass (FC-Widerruf, §4/§6) — dann ist Layer 2 mit weg.
- Layer 1 (NEURLFilter) ist bereits `shouldFailClosed = true` + es gibt Backend-Bypass-Detection (`recoveringFromBypass`-Phase, `/api/protection/state`, `mark-active`). Das Produkt hat also schon einen Mechanismus, der „NEURLFilter aus" *erkennt* und den User zur Re-Aktivierung *drängt*. Layer 2 dupliziert teilweise diesen Schutzgedanken, nur schwächer.
- **Honest-consultant-Fazit:** Layer 2 als „50er-Always-On-Fallback" verkauft ein Sicherheitsversprechen, das es nicht halten kann. Es ist „Defense in Depth light" — nett, aber kein echter zweiter Sicherheitsgurt. Wer es einbaut, sollte es intern und im UI als *„zusätzliche Härtung der bekanntesten Anbieter"* framen, **niemals** als „Schutz bleibt, wenn Layer 1 aus ist".
### Verbesserungsvorschläge / Alternativen
1. **Gambling-Kategorie statt 50er-Liste:** auf iOS **nicht möglich** (§1). Entfällt.
2. **`.auto` mitnehmen — kostenlos:** Statt `.specific(top50)` → `.auto([...top50...])`. `.auto` blockt zusätzlich **Adult Content** systemseitig gratis mit. Bei Spielsucht oft Begleit-Trigger; minimaler Mehraufwand, spürbarer Härtungs-Effekt. Trade-off: deaktiviert Safari-Private-Browsing (bei `.specific` aber ohnehin auch der Fall — *jede* Policy ≠ `.none` tut das).
3. **Statische Bundle-Liste > Backend-Endpoint** (§7). Datensparsam, offline-fähig, kein neuer Server-Code.
4. **Das eigentliche Loch zuerst schließen:** Der höhere Hebel ist **FC-Widerruf-Erkennung**. `AuthorizationCenter.shared.authorizationStatus` bei jedem App-Foreground prüfen → wenn nicht mehr `.approved` → aggressiver Nudge + Backend-Flag (analog `recoveringFromBypass`). Das adressiert §4/§6 direkt und ist mehr wert als Layer 2.
5. **Risiken:**
- **Falsches Sicherheitsgefühl** beim User („ich bin geschützt") — bei einem DiGA-/Suchthilfe-Produkt ein ernstes Thema. UI-Wording streng.
- **Private-Browsing-Deaktivierung in Safari** als Nebeneffekt — für die Zielgruppe vermutlich erwünscht, aber dokumentieren.
- **FC-Auth-Verbrauch:** Layer 2 hängt an derselben FC-Authorization wie `denyAppRemoval`. Kein neues Risiko, aber: ein einziger Widerruf killt beides.
6. **Aufwand grob:** klein. ~0,51 Tag. Ein neuer `AsyncFunction` im bestehenden Swift-Modul, ein gebundeltes JSON, JS-Bridge-Methode, ein Aufruf im Aktivierungs-Flow. Kein neues Entitlement, kein Plugin-Eingriff (FC + App-Group sind schon da).
### Wann es sich doch lohnt
Wenn das Team es als **bewusste, klein gehaltene Zusatz-Härtung** akzeptiert (nicht als Fallback-Versprechen) und parallel die FC-Widerruf-Erkennung baut — dann ist der Aufwand niedrig genug, dass „nice to have" vertretbar ist. Als *alleinige* Layer-2-Strategie: zu schwach.
---
## Implementierungs-Skizze (KEIN Code — nur Plan)
**Betroffene Dateien:**
| Datei | Änderung |
|---|---|
| `modules/rebreak-protection/ios/RebreakProtectionModule.swift` | Neuer `AsyncFunction("activateWebContentFilter")`: Land empfangen, Top-50 setzen via `ManagedSettingsStore(named: MS_STORE_NAME).webContent.blockedByFilter = .auto(domains)` bzw. `.specific(domains)`. Setzt voraus, dass FC bereits authorisiert ist. `disable` (Z. 368) erweitern: `webContent.blockedByFilter = .none` bzw. via `clearAllSettings()` (deckt es schon ab — prüfen). |
| `modules/rebreak-protection/src/RebreakProtection.types.ts` | Typ für die neue Bridge-Methode + ggf. `webContentFilter`-Layer in `DeviceLayers`. |
| `modules/rebreak-protection/src/RebreakProtectionModule.ts` | Bridge-Deklaration. |
| `modules/rebreak-protection/src/RebreakProtectionModule.web.ts` | No-op-Stub. |
| `lib/protection.ts` | Orchestrierung: nach `activateFamilyControls()` zusätzlich `activateWebContentFilter({country})` aufrufen; `getDeviceState`/`getCombinedState` ggf. um Layer-Status erweitern. |
| `assets/`-Bundle (neu) | `gambling-top50-by-country.json` (`{ "DE": [...], "GB": [...], ... }`). Mit der App gebundelt. |
| Backend | **Nicht nötig** bei Bundle-Variante. Nur falls Server-Updates gewünscht: `/api/url-filter/`-Bereich um `top50.json?country=` ergänzen. |
| `plugins/with-rebreak-protection-ios.js` | **Keine Änderung** — FC-Entitlement + App-Group sind bereits vorhanden. |
**Grobe Schritte:**
1. Top-50-Gambling-Domains pro Zielland kuratieren (DE/GB/FR zuerst — die i18n-Sprachen des Repos). Quelle: vorhandene 208k-PIR-Liste nach Land/Traffic-Rang filtern (`backend/scripts/generate-pir-input.ts` ist verwandter Kontext).
2. Land via `Locale.current.region` bestimmen (optional Profil-Override); JSON-Lookup.
3. `WebDomain`-Set bauen, `blockedByFilter` setzen (`.auto` empfohlen — Adult-Content gratis mit). FC-Auth-Status vorher prüfen.
4. Disable-Pfad: sicherstellen, dass `clearAllSettings()` auch `webContent` zurücksetzt (vermutlich ja — verifizieren), sonst explizit `.none` setzen.
5. **Empirisch testen auf iPhone (iOS 26, kein Simulator — MEMORY-Regel):** (a) blockt es eine Top-50-Domain in Safari? (b) auch in Chrome iOS? (c) verträgt es sich sichtbar mit aktivem NEURLFilter? (d) was passiert bei FC-Widerruf?
6. UI-Wording festlegen — **kein** „Fallback"-Versprechen.
---
## Offene Entscheidungen für den User
1. **Layer 2 überhaupt bauen?** Ehrliche Empfehlung: nur als bewusst kleingehaltene Zusatzhärtung — und nur **zusammen mit** FC-Widerruf-Erkennung. Als alleiniger „Fallback" zu schwach. → Deine Entscheidung.
2. **`.auto` (Adult-Content gratis mit) oder `.specific` (nur Gambling)?** `.auto` empfohlen, falls Adult-Content-Block für die Zielgruppe ok ist.
3. **Bundle-JSON oder Backend-Endpoint** für die Top-50? Empfehlung Bundle (datensparsam, offline). Backend nur falls Updates ohne App-Release wichtig.
4. **Welche Länder zum Start?** Vorschlag: DE, GB, FR (= vorhandene App-Sprachen).
5. **Landbestimmung:** Device-Region (empfohlen) vs. optionales Profil-Feld?
6. **Soll stattdessen/zuerst die FC-Widerruf-Erkennung gebaut werden?** Das ist nach dieser Recherche der höhere Hebel — und schließt das größere Loch, gegen das Layer 2 nicht hilft.
---
## Quellen
- [WebContentSettings | Apple Developer](https://developer.apple.com/documentation/managedsettings/webcontentsettings)
- [WebContentSettings.FilterPolicy | Apple Developer](https://developer.apple.com/documentation/managedsettings/webcontentsettings/filterpolicy)
- [blockedByFilter | Apple Developer](https://developer.apple.com/documentation/managedsettings/webcontentsettings/blockedbyfilter-swift.property) — *50-Domain-Limit + Private-Browsing-Hinweis*
- [FilterPolicy.specific(_:) | Apple Developer](https://developer.apple.com/documentation/managedsettings/webcontentsettings/filterpolicy/specific(_:))
- [FilterPolicy.auto(_:except:) | Apple Developer](https://developer.apple.com/documentation/managedsettings/webcontentsettings/filterpolicy/auto(_:except:))
- [ShieldSettings.webDomainCategories | Apple Developer](https://developer.apple.com/documentation/managedsettings/shieldsettings/webdomaincategories-swift.property) — *Shielding = UI-Overlay, kein Traffic-Block*
- [NEURLFilterManager | Apple Developer](https://developer.apple.com/documentation/NetworkExtension/NEURLFilterManager)
- [Filter and tunnel network traffic with NetworkExtension — WWDC25](https://developer.apple.com/videos/play/wwdc2025/234/)
- [iOS 26 Network Extension URL Filtering — dev.to/arshtechpro](https://dev.to/arshtechpro/ios-26-network-extension-url-filtering-revolution-for-enterprise-and-consumer-apps-40ij)
- [AuthorizationCenter | Apple Developer](https://developer.apple.com/documentation/familycontrols/authorizationcenter)
- [A Developer's Guide to Apple's Screen Time APIs — Medium](https://medium.com/@juliusbrussee/a-developers-guide-to-apple-s-screen-time-apis-familycontrols-managedsettings-deviceactivity-e660147367d7) — *Restrictions fallen lautlos bei Auth-Widerruf*