From 6cd3a78aaf941a62d843db1a8c02964f25874c94 Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Thu, 21 May 2026 16:07:44 +0200 Subject: [PATCH] =?UTF-8?q?feat(protection):=20iOS=20Layer-2=20webContent-?= =?UTF-8?q?Filter=20(ManagedSettings)=20=E2=80=94=20MVP-Plumbing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WebKit-interner Content-Filter via ManagedSettingsStore().webContent als stilles Sicherheitsnetz. Blockt eine kuratierte, laenderabhaengige Top- Gambling-Domain-Liste plus systemseitig Adult-Content (.auto-Variante). Braucht NUR Family Controls — kein MDM, kein neues Entitlement, keine Config-Plugin-Aenderung. - gambling-domains.json: gebuendelte Starter-Liste (DE/GB/FR), je <=50 Domains (Apple-Hartlimit), klar als STARTER markiert. Via Podspec- resource_bundles ins App-Bundle gepackt. - applyWebContentFilter / clearWebContentFilter: zwei native AsyncFunctions. Land via Locale.current.region, iOS 16+ gegated, FC-Auth vorausgesetzt. - JS-Bridge (Module-Decl, types, web-stub, lib/protection.ts) + Actions im useProtectionState-Hook. getDeviceState liefert webContentFilter-Layer mit. KEINE Auto-Trigger-Logik — Layer 2 ist vorerst nur explizit aufrufbare Capability. Siehe TODO(layer2-gating) im Swift-Modul und lib/protection.ts. Co-Authored-By: Claude Opus 4.7 --- .../assets/protection/gambling-domains.json | 95 ++++++++++ .../hooks/useProtectionState.ts | 27 +++ apps/rebreak-native/lib/protection.ts | 39 ++++ .../modules/rebreak-protection/index.ts | 1 + .../ios/RebreakProtection.podspec | 9 + .../ios/RebreakProtectionModule.swift | 173 +++++++++++++++++- .../src/RebreakProtection.types.ts | 18 ++ .../src/RebreakProtectionModule.ts | 20 ++ .../src/RebreakProtectionModule.web.ts | 9 + 9 files changed, 389 insertions(+), 2 deletions(-) create mode 100644 apps/rebreak-native/assets/protection/gambling-domains.json diff --git a/apps/rebreak-native/assets/protection/gambling-domains.json b/apps/rebreak-native/assets/protection/gambling-domains.json new file mode 100644 index 0000000..39fc94c --- /dev/null +++ b/apps/rebreak-native/assets/protection/gambling-domains.json @@ -0,0 +1,95 @@ +{ + "_comment": "STARTER — kuratierte Starter-Liste der bekanntesten Gambling-Domains pro Land. NICHT die Endliste. Die finale, traffic-rangbasierte Kuratierung (via Similarweb-Ranking / GGL-Whitelist für DE) ist noch offen. Apple-Hartlimit: max. 50 Domains pro Land — diese Grenze darf NIE überschritten werden. Schlüssel = ISO-3166-1-alpha-2-Ländercode (Locale.current.region). Werte = registrierbare Domains ohne Schema/Subdomain (ManagedSettings WebDomain matched die Domain inkl. Subdomains).", + "_meta": { + "version": 1, + "updatedAt": "2026-05-21", + "maxDomainsPerCountry": 50, + "status": "starter" + }, + "DE": [ + "tipico.de", + "tipico.com", + "bwin.de", + "bwin.com", + "interwetten.de", + "interwetten.com", + "betano.de", + "bet-at-home.com", + "sportwetten.de", + "merkur-bets.de", + "merkurbets.de", + "happybet.de", + "neobet.de", + "winamax.de", + "betway.de", + "admiralbet.de", + "oddset.de", + "lottohelden.de", + "lotto.de", + "lotto24.de", + "jackpot.de", + "drueckglueck.de", + "loewen-play.de", + "merkur24.com", + "casino.de", + "casinos.de", + "betsson.de", + "leovegas.de", + "lapalingo.com", + "sunmaker.de" + ], + "GB": [ + "bet365.com", + "williamhill.com", + "skybet.com", + "skyvegas.com", + "ladbrokes.com", + "coral.co.uk", + "paddypower.com", + "betfair.com", + "betfred.com", + "unibet.co.uk", + "888.com", + "888sport.com", + "888casino.com", + "betway.com", + "virginbet.com", + "boylesports.com", + "betvictor.com", + "10bet.com", + "mrgreen.com", + "casumo.com", + "leovegas.com", + "grosvenorcasinos.com", + "mecca-bingo.com", + "gala-bingo.com", + "tombola.co.uk", + "lottoland.co.uk", + "national-lottery.co.uk", + "kwikfit-pools.co.uk", + "parimatch.co.uk", + "smarkets.com" + ], + "FR": [ + "winamax.fr", + "betclic.fr", + "betclic.com", + "pmu.fr", + "unibet.fr", + "parionssport.fdj.fr", + "fdj.fr", + "zebet.fr", + "vbet.fr", + "netbet.fr", + "bwin.fr", + "genybet.fr", + "zeturf.fr", + "feeling-bet.fr", + "barrierebet.fr", + "pokerstars.fr", + "partypoker.fr", + "lucien-barriere.com", + "casinobarriere.com", + "circus.be" + ] +} diff --git a/apps/rebreak-native/hooks/useProtectionState.ts b/apps/rebreak-native/hooks/useProtectionState.ts index 6273694..d2b48ff 100644 --- a/apps/rebreak-native/hooks/useProtectionState.ts +++ b/apps/rebreak-native/hooks/useProtectionState.ts @@ -7,6 +7,7 @@ import { type ProtectionPhase, formatCooldownRemaining, } from '../lib/protection'; +import type { WebContentFilterResult } from '../modules/rebreak-protection'; const POLL_MS_ACTIVE_COOLDOWN = 5_000; const POLL_MS_NORMAL = 30_000; @@ -25,6 +26,15 @@ type UseProtectionStateReturn = { activateUrlFilter: () => Promise<{ enabled: boolean; error?: string }>; /** Aktiviert NUR Family Controls (= der Lock — danach nur per Cooldown abschaltbar). */ activateFamilyControls: () => Promise<{ enabled: boolean; error?: string }>; + /** + * iOS Layer 2 — aktiviert den webContent-Filter (kuratierte Gambling-Domain- + * Liste des Geräte-Landes). Stilles Sicherheitsnetz; braucht aktive Family + * Controls. KEINE Auto-Trigger-Logik — explizit aufrufbare Capability. + * No-op auf Android/Web. Siehe TODO(layer2-gating) in lib/protection.ts. + */ + applyWebContentFilter: () => Promise; + /** iOS Layer 2 — setzt den webContent-Filter zurück. Rührt den App-Lock nicht an. */ + clearWebContentFilter: () => Promise<{ cleared: boolean; error?: string }>; /** Startet 24h Cooldown via Backend. UI muss Friction-Flow vorher durchlaufen. */ requestDeactivation: (reason?: string) => Promise; /** Bricht laufenden Cooldown ab. Schutz bleibt aktiv. */ @@ -199,6 +209,21 @@ export function useProtectionState(): UseProtectionStateReturn { return result; }, [fetchState]); + // iOS Layer 2 — webContent-Filter. TODO(layer2-gating): bislang nur explizit + // aufrufbar; die Auto-Trigger-Logik (an wenn NEURLFilter aus + Cooldown läuft) + // ist eine offene User-Design-Entscheidung. + const applyWebContentFilter = useCallback(async () => { + const result = await protection.applyWebContentFilter(); + await fetchState(false); + return result; + }, [fetchState]); + + const clearWebContentFilter = useCallback(async () => { + const result = await protection.clearWebContentFilter(); + await fetchState(false); + return result; + }, [fetchState]); + const requestDeactivation = useCallback( async (reason?: string) => { await protection.requestDeactivation(reason); @@ -221,6 +246,8 @@ export function useProtectionState(): UseProtectionStateReturn { activate, activateUrlFilter, activateFamilyControls, + applyWebContentFilter, + clearWebContentFilter, requestDeactivation, cancelDeactivation, }; diff --git a/apps/rebreak-native/lib/protection.ts b/apps/rebreak-native/lib/protection.ts index e9ac699..90112a3 100644 --- a/apps/rebreak-native/lib/protection.ts +++ b/apps/rebreak-native/lib/protection.ts @@ -20,6 +20,7 @@ import type { SyncBlocklistOpts, SyncBlocklistResult, SystemSettingsTarget, + WebContentFilterResult, } from "../modules/rebreak-protection"; import { apiFetch } from "./api"; @@ -190,6 +191,44 @@ export const protection = { return RebreakProtection.getDeviceState(); }, + // ─── iOS Layer 2 — webContent-Filter (ManagedSettings) ─────────────────── + // + // Stilles WebKit-Sicherheitsnetz: blockt eine kuratierte, länderabhängige + // Top-Gambling-Domain-Liste (≤50 Domains — Apple-Hartlimit) via + // ManagedSettings. Braucht NUR Family Controls (kein MDM, kein neues + // Entitlement). Auf Android/Web no-op. + // + // TODO(layer2-gating): Aktuell NUR explizit aufrufbare Capability. Die + // Auto-Trigger-Logik ("Layer 2 automatisch AN sobald NEURLFilter/Layer 1 + // aus ist + ein Cooldown läuft, sonst AUS") ist bewusst NICHT gebaut — sie + // hängt an einer zuverlässigen NEURLFilter-Status-Erkennung (die aktuell + // selbst nicht stabil funktioniert) und ist eine offene Produkt-/Design- + // Entscheidung des Users. Bis dahin müssen Aufrufer applyWebContentFilter / + // clearWebContentFilter explizit triggern. + + /** + * Aktiviert Layer 2 — der webContent-Filter blockt die gebündelte Top- + * Gambling-Domain-Liste des Geräte-Landes. Setzt eine aktive Family-Controls- + * Authorization voraus. No-op auf Android/Web (gibt enabled:false zurück). + */ + async applyWebContentFilter(): Promise { + if (Platform.OS !== "ios") { + return { enabled: false, appliedCount: 0, region: "", error: "ios_only" }; + } + return RebreakProtection.applyWebContentFilter(); + }, + + /** + * Setzt Layer 2 zurück (blockedByFilter = .none). Rührt den App-Lock + * (denyAppRemoval) NICHT an. No-op auf Android/Web. + */ + async clearWebContentFilter(): Promise<{ cleared: boolean; error?: string }> { + if (Platform.OS !== "ios") { + return { cleared: false, error: "ios_only" }; + } + return RebreakProtection.clearWebContentFilter(); + }, + /** Android: VpnService neu starten falls er laufen sollte (`filter_enabled`) * aber tot ist (Reinstall / OS-Kill). Bei App-Start/Foreground aufrufen, * damit der State nicht „an aber tot" bleibt. No-op auf iOS/Web. */ diff --git a/apps/rebreak-native/modules/rebreak-protection/index.ts b/apps/rebreak-native/modules/rebreak-protection/index.ts index 19650f0..e766c44 100644 --- a/apps/rebreak-native/modules/rebreak-protection/index.ts +++ b/apps/rebreak-native/modules/rebreak-protection/index.ts @@ -12,6 +12,7 @@ export type { SyncBlocklistOpts, SyncBlocklistResult, SystemSettingsTarget, + WebContentFilterResult, } from './src/RebreakProtection.types'; export default RebreakProtectionModule; diff --git a/apps/rebreak-native/modules/rebreak-protection/ios/RebreakProtection.podspec b/apps/rebreak-native/modules/rebreak-protection/ios/RebreakProtection.podspec index b2d8281..d995fb4 100644 --- a/apps/rebreak-native/modules/rebreak-protection/ios/RebreakProtection.podspec +++ b/apps/rebreak-native/modules/rebreak-protection/ios/RebreakProtection.podspec @@ -23,4 +23,13 @@ Pod::Spec.new do |s| # Xcode-Target gepackt, NICHT in die Hauptmodul-Lib. s.source_files = '*.{h,m,mm,swift,hpp,cpp}' s.exclude_files = 'RebreakURLFilter/**/*' + + # Layer-2 (webContent-Filter): gebündelte Gambling-Domain-Liste pro Land. + # Wird als eigenständiges Resource-Bundle (RebreakProtectionResources.bundle) + # ins App-Bundle gepackt — RebreakProtectionModule.swift liest es zur Laufzeit + # via Bundle(for:)/url(forResource:). Pfad relativ zur Podspec → zeigt auf die + # von der Aufgabe vorgegebene Quelle apps/rebreak-native/assets/protection/. + s.resource_bundles = { + 'RebreakProtectionResources' => ['../../../assets/protection/gambling-domains.json'] + } end diff --git a/apps/rebreak-native/modules/rebreak-protection/ios/RebreakProtectionModule.swift b/apps/rebreak-native/modules/rebreak-protection/ios/RebreakProtectionModule.swift index 8909503..db32518 100644 --- a/apps/rebreak-native/modules/rebreak-protection/ios/RebreakProtectionModule.swift +++ b/apps/rebreak-native/modules/rebreak-protection/ios/RebreakProtectionModule.swift @@ -16,6 +16,14 @@ private let LAST_SYNC_KEY = "blocklist_last_sync_at" private let DARWIN_NOTIF = "rebreak.blocklist.updated" private let MS_STORE_NAME = "rebreak.shield" +// Layer 2 — webContent-Filter (ManagedSettings) +private let WEBCONTENT_BUNDLE = "RebreakProtectionResources" +private let WEBCONTENT_DOMAINS_FILE = "gambling-domains" +// Apple-Hartlimit: ManagedSettings blockt max. 50 Domains pro FilterPolicy. +// Wird hier defensiv durchgesetzt — nie mehr als 50 an blockedByFilter geben. +private let WEBCONTENT_MAX_DOMAINS = 50 +private let WEBCONTENT_FALLBACK_REGION = "DE" + // ─── Shared Log-Store ───────────────────────────────────────────────────────── fileprivate enum SharedLogStore { @@ -319,11 +327,12 @@ public class RebreakProtectionModule: Module { SharedLogStore.append("⚠️ NEFilter disable: \(error.localizedDescription)") } - // ManagedSettings (löst denyAppRemoval) + // ManagedSettings (löst denyAppRemoval UND Layer-2-webContent-Filter — + // clearAllSettings() setzt den gesamten Store inkl. webContent zurück). if #available(iOS 16.0, *) { let store = ManagedSettingsStore(named: ManagedSettingsStore.Name(rawValue: MS_STORE_NAME)) store.clearAllSettings() - SharedLogStore.append("🔓 ManagedSettings cleared") + SharedLogStore.append("🔓 ManagedSettings cleared (denyAppRemoval + webContent)") } // Blocklist-Datei löschen @@ -332,6 +341,104 @@ public class RebreakProtectionModule: Module { return ["allLayersOff": true] } + // ───────── Layer 2: webContent-Filter (ManagedSettings) ───────── + // + // WebKit-interner Content-Filter via ManagedSettingsStore().webContent. + // Stilles Sicherheitsnetz: blockt eine kuratierte, länderabhängige Top- + // Gambling-Domain-Liste (gebündeltes JSON, ≤50 Domains pro Land — Apple- + // Hartlimit) plus systemseitig Adult-Content gratis mit (.auto-Variante). + // + // Voraussetzung: gültige Family-Controls-Authorization (wie denyAppRemoval). + // Kein MDM, kein neues Entitlement. Wirkt nur, solange FC authorisiert ist — + // bei FC-Widerruf fällt der Filter lautlos weg (Apple-Verhalten, kein + // Callback). + // + // TODO(layer2-gating): Aktuell NUR explizit aufrufbare Capability. Die + // Auto-Trigger-Logik ("Layer 2 automatisch AN sobald NEURLFilter/Layer 1 + // deaktiviert ist + während ein Cooldown läuft, sonst AUS") ist bewusst + // NICHT implementiert — sie hängt an einer zuverlässigen NEURLFilter- + // Status-Erkennung (die aktuell selbst nicht stabil funktioniert) und ist + // eine offene Produkt-/Design-Entscheidung des Users. Bis dahin ruft die + // JS-Schicht applyWebContentFilter/clearWebContentFilter explizit auf. + + // ───────── applyWebContentFilter: Land → Domains → blockedByFilter ───────── + + AsyncFunction("applyWebContentFilter") { () async -> [String: Any] in + var error: String? = nil + var enabled = false + var appliedCount = 0 + var resolvedRegion = WEBCONTENT_FALLBACK_REGION + + if #available(iOS 16.0, *) { + // Family Controls muss authorisiert sein, sonst ist webContent stumm. + guard AuthorizationCenter.shared.authorizationStatus == .approved else { + SharedLogStore.append("⚠️ [applyWebContentFilter] FamilyControls nicht authorisiert — abort") + return [ + "enabled": false, + "appliedCount": 0, + "region": resolvedRegion, + "error": "family_controls_not_authorized", + ] + } + + // 1) Land bestimmen — Locale.current.region (Fallback: erstes Element, + // sonst "DE"). region ist iOS 16+; .regionCode als Pre-16-Fallback. + if let region = Locale.current.region?.identifier, !region.isEmpty { + resolvedRegion = region.uppercased() + } else if let region = Locale.current.regionCode, !region.isEmpty { + resolvedRegion = region.uppercased() + } + SharedLogStore.append("🌍 [applyWebContentFilter] region=\(resolvedRegion)") + + // 2) Domain-Liste für das Land aus dem gebündelten JSON laden. + let domains = Self.loadWebContentDomains(forRegion: resolvedRegion) + if domains.isEmpty { + SharedLogStore.append("⚠️ [applyWebContentFilter] keine Domains für \(resolvedRegion)") + return [ + "enabled": false, + "appliedCount": 0, + "region": resolvedRegion, + "error": "no_domains_for_region", + ] + } + + // 3) blockedByFilter setzen. .auto(_, except:) blockt die gelisteten + // Domains PLUS systemseitig Adult-Content gratis mit. Hartlimit 50. + let webDomains = Set(domains.prefix(WEBCONTENT_MAX_DOMAINS).map { WebDomain(domain: $0) }) + appliedCount = webDomains.count + + let store = ManagedSettingsStore(named: ManagedSettingsStore.Name(rawValue: MS_STORE_NAME)) + store.webContent.blockedByFilter = .auto(webDomains, except: []) + enabled = true + SharedLogStore.append("🛡️ [applyWebContentFilter] webContent.blockedByFilter=.auto — \(appliedCount) Domains (\(resolvedRegion))") + } else { + error = "iOS 16+ required for webContent filter" + SharedLogStore.append("❌ [applyWebContentFilter] \(error!)") + } + + var result: [String: Any] = [ + "enabled": enabled, + "appliedCount": appliedCount, + "region": resolvedRegion, + ] + if let error = error { result["error"] = error } + return result + } + + // ───────── clearWebContentFilter: Filter zurücksetzen ───────── + + AsyncFunction("clearWebContentFilter") { () async -> [String: Any] in + if #available(iOS 16.0, *) { + let store = ManagedSettingsStore(named: ManagedSettingsStore.Name(rawValue: MS_STORE_NAME)) + // Nur den webContent-Layer zurücksetzen — NICHT clearAllSettings(), + // damit denyAppRemoval (Layer „App-Lock") unangetastet bleibt. + store.webContent.blockedByFilter = .none + SharedLogStore.append("🔓 [clearWebContentFilter] webContent.blockedByFilter=.none") + return ["cleared": true] + } + return ["cleared": false, "error": "iOS 16+ required"] + } + // ───────── getDeviceState: aktueller Status aller Layer ───────── AsyncFunction("getDeviceState") { () async -> [String: Any] in @@ -348,10 +455,19 @@ public class RebreakProtectionModule: Module { // FamilyControls var familyControls = false var appDeletionLock = false + var webContentFilter = false if #available(iOS 16.0, *) { familyControls = AuthorizationCenter.shared.authorizationStatus == .approved let store = ManagedSettingsStore(named: ManagedSettingsStore.Name(rawValue: MS_STORE_NAME)) appDeletionLock = (store.application.denyAppRemoval as? Bool) == true + // Layer 2 gilt als „an", wenn eine FilterPolicy ≠ .none gesetzt ist. + if let policy = store.webContent.blockedByFilter { + if case .none = policy { + webContentFilter = false + } else { + webContentFilter = true + } + } } let count = Self.currentHashCount() @@ -361,6 +477,7 @@ public class RebreakProtectionModule: Module { "urlFilter": urlFilter, "familyControls": familyControls, "appDeletionLock": appDeletionLock, + "webContentFilter": webContentFilter, "blocklistCount": count, "blocklistLastSyncAt": lastSync ?? NSNull(), ] @@ -609,6 +726,58 @@ public class RebreakProtectionModule: Module { // ─── Helpers ──────────────────────────────────────────────────────────────── + /// Lädt die kuratierte Gambling-Domain-Liste für ein Land aus dem + /// gebündelten JSON (gambling-domains.json). Das JSON wird von der Podspec + /// als RebreakProtectionResources.bundle ins App-Bundle gepackt. + /// + /// Struktur: `{ "DE": ["..."], "GB": [...], ... }` plus `_comment`/`_meta`- + /// Felder, die hier ignoriert werden. Liefert [] wenn das Bundle/JSON fehlt + /// oder das Land nicht gelistet ist — der Aufrufer behandelt das als + /// no_domains_for_region. + private static func loadWebContentDomains(forRegion region: String) -> [String] { + // Resource-Bundle innerhalb des Frameworks/Pods auflösen. + let moduleBundle = Bundle(for: RebreakProtectionModule.self) + var jsonURL: URL? = moduleBundle.url( + forResource: WEBCONTENT_DOMAINS_FILE, withExtension: "json") + + if jsonURL == nil, + let resourceBundleURL = moduleBundle.url( + forResource: WEBCONTENT_BUNDLE, withExtension: "bundle"), + let resourceBundle = Bundle(url: resourceBundleURL) { + jsonURL = resourceBundle.url( + forResource: WEBCONTENT_DOMAINS_FILE, withExtension: "json") + } + // Fallback: manche Build-Konfigs flachen Resource-Bundles ins Main-Bundle. + if jsonURL == nil { + jsonURL = Bundle.main.url( + forResource: WEBCONTENT_DOMAINS_FILE, withExtension: "json") + } + + guard let url = jsonURL, + let data = try? Data(contentsOf: url), + let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any] + else { + SharedLogStore.append("❌ [loadWebContentDomains] \(WEBCONTENT_DOMAINS_FILE).json nicht ladbar") + return [] + } + + guard let domains = root[region.uppercased()] as? [String] else { + return [] + } + // Defensiv normalisieren: leere Strings raus, lowercase, dedupliziert, + // hart auf das Apple-Limit gekappt. + var seen = Set() + var cleaned: [String] = [] + for raw in domains { + let d = raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + if d.isEmpty || seen.contains(d) { continue } + seen.insert(d) + cleaned.append(d) + if cleaned.count >= WEBCONTENT_MAX_DOMAINS { break } + } + return cleaned + } + private static func currentHashCount() -> Int { guard let url = FileManager.default .containerURL(forSecurityApplicationGroupIdentifier: APP_GROUP)? diff --git a/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtection.types.ts b/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtection.types.ts index f0a652e..c9a3261 100644 --- a/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtection.types.ts +++ b/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtection.types.ts @@ -9,6 +9,11 @@ export type DeviceLayers = { urlFilter?: boolean; familyControls?: boolean; appDeletionLock?: boolean; + /** + * Layer 2 — iOS-only. True wenn der ManagedSettings-webContent-Filter eine + * FilterPolicy ≠ .none gesetzt hat (kuratierte Gambling-Domain-Liste aktiv). + */ + webContentFilter?: boolean; // Android vpn?: boolean; accessibility?: boolean; @@ -18,6 +23,19 @@ export type DeviceLayers = { blocklistLastSyncAt: string | null; }; +/** + * Ergebnis von applyWebContentFilter (iOS Layer 2). + * `enabled` = Filter wurde gesetzt. `appliedCount` = wie viele Domains + * tatsächlich an blockedByFilter gingen (≤50, Apple-Hartlimit). + * `region` = das per Locale.current.region bestimmte Land. + */ +export type WebContentFilterResult = { + enabled: boolean; + appliedCount: number; + region: string; + error?: string; +}; + export type ActivateResult = { allLayersOn: boolean; /** diff --git a/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.ts b/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.ts index 8533dd8..306de2f 100644 --- a/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.ts +++ b/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.ts @@ -10,6 +10,7 @@ import type { SyncBlocklistOpts, SyncBlocklistResult, SystemSettingsTarget, + WebContentFilterResult, } from './RebreakProtection.types'; declare class RebreakProtectionModule extends NativeModule { @@ -52,6 +53,25 @@ declare class RebreakProtectionModule extends NativeModule; + /** + * iOS Layer 2 — webContent-Filter (ManagedSettings). Bestimmt das Land via + * Locale.current.region, lädt die gebündelte Top-Gambling-Domain-Liste für + * dieses Land (≤50 Domains, Apple-Hartlimit) und setzt + * `ManagedSettingsStore().webContent.blockedByFilter = .auto(...)` — blockt + * die Domains in WebKit (Safari u.a.) plus systemseitig Adult-Content. + * + * Setzt eine gültige Family-Controls-Authorization voraus (wie der App-Lock). + * Auf Android/iOS<16 no-op. Stilles Sicherheitsnetz; KEINE Auto-Trigger-Logik + * — muss explizit aufgerufen werden (siehe TODO(layer2-gating) im Swift-Modul). + */ + applyWebContentFilter(): Promise; + + /** + * iOS Layer 2 — setzt den webContent-Filter zurück (blockedByFilter = .none). + * Rührt denyAppRemoval (App-Lock) NICHT an. Auf Android/iOS<16 no-op. + */ + clearWebContentFilter(): Promise<{ cleared: boolean; error?: string }>; + /** Aktueller Device-State. Polling- und Health-Check-Pfad. */ getDeviceState(): Promise; diff --git a/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.web.ts b/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.web.ts index 8869f12..895ba50 100644 --- a/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.web.ts +++ b/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.web.ts @@ -26,6 +26,15 @@ class RebreakProtectionModuleWeb extends NativeModule { return { blocklistCount: 0, blocklistLastSyncAt: null }; } + // iOS Layer 2 — webContent-Filter. Auf Web inhärent ohne Funktion. + async applyWebContentFilter() { + return { enabled: false, appliedCount: 0, region: '', error: 'web_stub' }; + } + + async clearWebContentFilter() { + return { cleared: false, error: 'web_stub' }; + } + async syncBlocklist(): Promise { return { updated: false, count: 0 }; }