Merge branch 'feat/ios-webcontent-layer2' — iOS Schutz-Layer-2 (webContent)

WebKit webContent-Filter via ManagedSettings (MVP-Plumbing):
applyWebContentFilter/clearWebContentFilter, gebuendelte Gambling-Domain-Liste
DE/GB/FR (Starter), JS-Bridge + Hook. Braucht nur Family Controls.
Auto-Trigger-Gating bewusst offen — TODO(layer2-gating).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-05-21 18:11:23 +02:00
commit 627ddce995
9 changed files with 389 additions and 2 deletions

View File

@ -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"
]
}

View File

@ -7,6 +7,7 @@ import {
type ProtectionPhase, type ProtectionPhase,
formatCooldownRemaining, formatCooldownRemaining,
} from '../lib/protection'; } from '../lib/protection';
import type { WebContentFilterResult } from '../modules/rebreak-protection';
const POLL_MS_ACTIVE_COOLDOWN = 5_000; const POLL_MS_ACTIVE_COOLDOWN = 5_000;
const POLL_MS_NORMAL = 30_000; const POLL_MS_NORMAL = 30_000;
@ -25,6 +26,15 @@ type UseProtectionStateReturn = {
activateUrlFilter: () => Promise<{ enabled: boolean; error?: string }>; activateUrlFilter: () => Promise<{ enabled: boolean; error?: string }>;
/** Aktiviert NUR Family Controls (= der Lock — danach nur per Cooldown abschaltbar). */ /** Aktiviert NUR Family Controls (= der Lock — danach nur per Cooldown abschaltbar). */
activateFamilyControls: () => Promise<{ enabled: boolean; error?: string }>; 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<WebContentFilterResult>;
/** 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. */ /** Startet 24h Cooldown via Backend. UI muss Friction-Flow vorher durchlaufen. */
requestDeactivation: (reason?: string) => Promise<void>; requestDeactivation: (reason?: string) => Promise<void>;
/** Bricht laufenden Cooldown ab. Schutz bleibt aktiv. */ /** Bricht laufenden Cooldown ab. Schutz bleibt aktiv. */
@ -199,6 +209,21 @@ export function useProtectionState(): UseProtectionStateReturn {
return result; return result;
}, [fetchState]); }, [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( const requestDeactivation = useCallback(
async (reason?: string) => { async (reason?: string) => {
await protection.requestDeactivation(reason); await protection.requestDeactivation(reason);
@ -221,6 +246,8 @@ export function useProtectionState(): UseProtectionStateReturn {
activate, activate,
activateUrlFilter, activateUrlFilter,
activateFamilyControls, activateFamilyControls,
applyWebContentFilter,
clearWebContentFilter,
requestDeactivation, requestDeactivation,
cancelDeactivation, cancelDeactivation,
}; };

View File

@ -20,6 +20,7 @@ import type {
SyncBlocklistOpts, SyncBlocklistOpts,
SyncBlocklistResult, SyncBlocklistResult,
SystemSettingsTarget, SystemSettingsTarget,
WebContentFilterResult,
} from "../modules/rebreak-protection"; } from "../modules/rebreak-protection";
import { apiFetch } from "./api"; import { apiFetch } from "./api";
@ -209,6 +210,44 @@ export const protection = {
return RebreakProtection.getDeviceState(); 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<WebContentFilterResult> {
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`) /** Android: VpnService neu starten falls er laufen sollte (`filter_enabled`)
* aber tot ist (Reinstall / OS-Kill). Bei App-Start/Foreground aufrufen, * aber tot ist (Reinstall / OS-Kill). Bei App-Start/Foreground aufrufen,
* damit der State nicht an aber tot" bleibt. No-op auf iOS/Web. */ * damit der State nicht an aber tot" bleibt. No-op auf iOS/Web. */

View File

@ -12,6 +12,7 @@ export type {
SyncBlocklistOpts, SyncBlocklistOpts,
SyncBlocklistResult, SyncBlocklistResult,
SystemSettingsTarget, SystemSettingsTarget,
WebContentFilterResult,
} from './src/RebreakProtection.types'; } from './src/RebreakProtection.types';
export default RebreakProtectionModule; export default RebreakProtectionModule;

View File

@ -23,4 +23,13 @@ Pod::Spec.new do |s|
# Xcode-Target gepackt, NICHT in die Hauptmodul-Lib. # Xcode-Target gepackt, NICHT in die Hauptmodul-Lib.
s.source_files = '*.{h,m,mm,swift,hpp,cpp}' s.source_files = '*.{h,m,mm,swift,hpp,cpp}'
s.exclude_files = 'RebreakURLFilter/**/*' 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 end

View File

@ -16,6 +16,14 @@ private let LAST_SYNC_KEY = "blocklist_last_sync_at"
private let DARWIN_NOTIF = "rebreak.blocklist.updated" private let DARWIN_NOTIF = "rebreak.blocklist.updated"
private let MS_STORE_NAME = "rebreak.shield" 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 // Shared Log-Store
fileprivate enum SharedLogStore { fileprivate enum SharedLogStore {
@ -395,11 +403,12 @@ public class RebreakProtectionModule: Module {
} }
} }
// 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, *) { if #available(iOS 16.0, *) {
let store = ManagedSettingsStore(named: ManagedSettingsStore.Name(rawValue: MS_STORE_NAME)) let store = ManagedSettingsStore(named: ManagedSettingsStore.Name(rawValue: MS_STORE_NAME))
store.clearAllSettings() store.clearAllSettings()
SharedLogStore.append("🔓 ManagedSettings cleared") SharedLogStore.append("🔓 ManagedSettings cleared (denyAppRemoval + webContent)")
} }
// Blocklist-Datei löschen // Blocklist-Datei löschen
@ -408,6 +417,104 @@ public class RebreakProtectionModule: Module {
return ["allLayersOff": true] 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 // getDeviceState: aktueller Status aller Layer
AsyncFunction("getDeviceState") { () async -> [String: Any] in AsyncFunction("getDeviceState") { () async -> [String: Any] in
@ -428,10 +535,19 @@ public class RebreakProtectionModule: Module {
// FamilyControls // FamilyControls
var familyControls = false var familyControls = false
var appDeletionLock = false var appDeletionLock = false
var webContentFilter = false
if #available(iOS 16.0, *) { if #available(iOS 16.0, *) {
familyControls = AuthorizationCenter.shared.authorizationStatus == .approved familyControls = AuthorizationCenter.shared.authorizationStatus == .approved
let store = ManagedSettingsStore(named: ManagedSettingsStore.Name(rawValue: MS_STORE_NAME)) let store = ManagedSettingsStore(named: ManagedSettingsStore.Name(rawValue: MS_STORE_NAME))
appDeletionLock = (store.application.denyAppRemoval as? Bool) == true 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() let count = Self.currentHashCount()
@ -441,6 +557,7 @@ public class RebreakProtectionModule: Module {
"urlFilter": urlFilter, "urlFilter": urlFilter,
"familyControls": familyControls, "familyControls": familyControls,
"appDeletionLock": appDeletionLock, "appDeletionLock": appDeletionLock,
"webContentFilter": webContentFilter,
"blocklistCount": count, "blocklistCount": count,
"blocklistLastSyncAt": lastSync ?? NSNull(), "blocklistLastSyncAt": lastSync ?? NSNull(),
] ]
@ -689,6 +806,58 @@ public class RebreakProtectionModule: Module {
// Helpers // 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<String>()
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 { private static func currentHashCount() -> Int {
guard let url = FileManager.default guard let url = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: APP_GROUP)? .containerURL(forSecurityApplicationGroupIdentifier: APP_GROUP)?

View File

@ -9,6 +9,11 @@ export type DeviceLayers = {
urlFilter?: boolean; urlFilter?: boolean;
familyControls?: boolean; familyControls?: boolean;
appDeletionLock?: 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 // Android
vpn?: boolean; vpn?: boolean;
accessibility?: boolean; accessibility?: boolean;
@ -18,6 +23,19 @@ export type DeviceLayers = {
blocklistLastSyncAt: string | null; 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 = { export type ActivateResult = {
allLayersOn: boolean; allLayersOn: boolean;
/** /**

View File

@ -10,6 +10,7 @@ import type {
SyncBlocklistOpts, SyncBlocklistOpts,
SyncBlocklistResult, SyncBlocklistResult,
SystemSettingsTarget, SystemSettingsTarget,
WebContentFilterResult,
} from './RebreakProtection.types'; } from './RebreakProtection.types';
declare class RebreakProtectionModule extends NativeModule<RebreakProtectionEvents> { declare class RebreakProtectionModule extends NativeModule<RebreakProtectionEvents> {
@ -56,6 +57,25 @@ declare class RebreakProtectionModule extends NativeModule<RebreakProtectionEven
*/ */
disable(): Promise<DisableResult>; disable(): Promise<DisableResult>;
/**
* 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<WebContentFilterResult>;
/**
* 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. */ /** Aktueller Device-State. Polling- und Health-Check-Pfad. */
getDeviceState(): Promise<DeviceLayers>; getDeviceState(): Promise<DeviceLayers>;

View File

@ -26,6 +26,15 @@ class RebreakProtectionModuleWeb extends NativeModule<RebreakProtectionEvents> {
return { blocklistCount: 0, blocklistLastSyncAt: null }; 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<SyncBlocklistResult> { async syncBlocklist(): Promise<SyncBlocklistResult> {
return { updated: false, count: 0 }; return { updated: false, count: 0 };
} }