feat(protection): runtime-Sync + cache-first fuer iOS Layer-2-Domain-Liste
syncWebContentDomains (gespiegelt von syncBlocklist): holt die Domain-Liste vom Backend, cached sie als webcontent-domains.json im App-Group-Container, ETag/304, Reapply nach Sync wenn FC aktiv. loadWebContentDomains liest cache-first, faellt auf die gebuendelte gambling-domains.json zurueck (Offline-Seed). Getriggert am selben Punkt wie syncBlocklist (useBlocklistSync). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
86ed603a45
commit
cc2d963d1f
@ -1,4 +1,5 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
import Constants from 'expo-constants';
|
||||
import { supabase } from '../lib/supabase';
|
||||
import { protection } from '../lib/protection';
|
||||
@ -16,6 +17,13 @@ type SyncResult = { ok: boolean; count?: number; plan?: string; error?: string }
|
||||
* - bei App-Resume (in case Server-Updates kamen)
|
||||
*
|
||||
* Backend respondet 304 wenn ETag matched → kein Re-Download.
|
||||
*
|
||||
* iOS-Layer-2: am selben Trigger wird auch die kuratierte webContent-Gambling-
|
||||
* Domain-Liste vom Backend gesynct (`syncWebContentDomains` →
|
||||
* `webcontent-domains.json` im App-Group-Cache). Best-effort und entkoppelt:
|
||||
* solange der Layer-2-Endpoint nicht deployed ist, schlägt dieser Sync fehl —
|
||||
* das beeinflusst das Blocklist-Sync-Ergebnis NICHT (der native
|
||||
* loadWebContentDomains fällt sauber auf die gebündelte JSON zurück).
|
||||
*/
|
||||
export function useBlocklistSync() {
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
@ -35,6 +43,24 @@ export function useBlocklistSync() {
|
||||
return result;
|
||||
}
|
||||
|
||||
// iOS-Layer-2: webContent-Domain-Liste am selben Trigger mitsyncen.
|
||||
// Bewusst NICHT awaited mit dem Blocklist-Sync gekoppelt — ein
|
||||
// Fehlschlag (z.B. Endpoint noch nicht deployed) darf das Blocklist-
|
||||
// Ergebnis nicht kippen. Fallback auf die gebündelte JSON greift nativ.
|
||||
if (Platform.OS === 'ios') {
|
||||
protection
|
||||
.syncWebContentDomains({ baseURL, authToken })
|
||||
.then((res) =>
|
||||
console.log('[webcontent-sync] ok:', JSON.stringify(res)),
|
||||
)
|
||||
.catch((e: any) =>
|
||||
console.warn(
|
||||
'[webcontent-sync] failed (bundled fallback active):',
|
||||
e?.message ?? e,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const res = await protection.syncBlocklist({ baseURL, authToken });
|
||||
const result = { ok: true, count: res.count, plan: res.plan };
|
||||
setLastResult(result);
|
||||
|
||||
@ -19,6 +19,8 @@ import type {
|
||||
HealthProbeResult,
|
||||
SyncBlocklistOpts,
|
||||
SyncBlocklistResult,
|
||||
SyncWebContentDomainsOpts,
|
||||
SyncWebContentDomainsResult,
|
||||
SystemSettingsTarget,
|
||||
WebContentFilterResult,
|
||||
} from "../modules/rebreak-protection";
|
||||
@ -248,6 +250,26 @@ export const protection = {
|
||||
return RebreakProtection.clearWebContentFilter();
|
||||
},
|
||||
|
||||
/**
|
||||
* Synct die kuratierte Layer-2-Gambling-Domain-Liste vom Backend in den
|
||||
* App-Group-Cache (`webcontent-domains.json`). Die gebündelte JSON bleibt
|
||||
* Offline-Seed/Fallback. Nach erfolgreichem Sync wird der webContent-Filter
|
||||
* — wenn Family Controls authorisiert ist — nativ sofort reapplied.
|
||||
*
|
||||
* No-op auf Android/Web (Layer 2 ist iOS-only) → gibt updated:false zurück.
|
||||
* Best-effort: solange der Backend-Endpoint nicht deployed ist, schlägt der
|
||||
* Fetch fehl — der native loadWebContentDomains fällt dann sauber auf die
|
||||
* gebündelte JSON zurück. Der Aufrufer behandelt einen Fehler als nicht-fatal.
|
||||
*/
|
||||
async syncWebContentDomains(
|
||||
opts: SyncWebContentDomainsOpts,
|
||||
): Promise<SyncWebContentDomainsResult> {
|
||||
if (Platform.OS !== "ios") {
|
||||
return { updated: false };
|
||||
}
|
||||
return RebreakProtection.syncWebContentDomains(opts);
|
||||
},
|
||||
|
||||
/** 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. */
|
||||
|
||||
@ -11,6 +11,8 @@ export type {
|
||||
RebreakProtectionEvents,
|
||||
SyncBlocklistOpts,
|
||||
SyncBlocklistResult,
|
||||
SyncWebContentDomainsOpts,
|
||||
SyncWebContentDomainsResult,
|
||||
SystemSettingsTarget,
|
||||
WebContentFilterResult,
|
||||
} from './src/RebreakProtection.types';
|
||||
|
||||
@ -23,6 +23,12 @@ private let WEBCONTENT_DOMAINS_FILE = "gambling-domains"
|
||||
// Wird hier defensiv durchgesetzt — nie mehr als 50 an blockedByFilter geben.
|
||||
private let WEBCONTENT_MAX_DOMAINS = 50
|
||||
private let WEBCONTENT_FALLBACK_REGION = "DE"
|
||||
// Runtime-Sync-Cache: die vom Backend geholte Domain-Liste landet als
|
||||
// webcontent-domains.json im App-Group-Container. loadWebContentDomains liest
|
||||
// cache-first; die gebündelte gambling-domains.json bleibt Offline-Seed/Fallback.
|
||||
private let WEBCONTENT_CACHE_FILENAME = "webcontent-domains.json"
|
||||
private let WEBCONTENT_ETAG_KEY = "webcontent_domains_etag"
|
||||
private let WEBCONTENT_LAST_SYNC_KEY = "webcontent_domains_last_sync_at"
|
||||
|
||||
// ─── Shared Log-Store ─────────────────────────────────────────────────────────
|
||||
|
||||
@ -675,6 +681,176 @@ public class RebreakProtectionModule: Module {
|
||||
)
|
||||
}
|
||||
|
||||
// ───────── syncWebContentDomains: Layer-2-Domain-Liste vom Backend ─────────
|
||||
//
|
||||
// Gespiegelt von `syncBlocklist` (oben). Holt die kuratierte Gambling-
|
||||
// Domain-Liste (`GET /api/protection/webcontent-domains`) und schreibt das
|
||||
// JSON atomar in den App-Group-Container als `webcontent-domains.json`.
|
||||
// `loadWebContentDomains` liest danach cache-first aus dieser Datei; die
|
||||
// gebündelte `gambling-domains.json` bleibt nur noch Offline-Seed/Fallback.
|
||||
//
|
||||
// URL-/Auth-Handling exakt wie `syncBlocklist`: baseURL + authToken kommen
|
||||
// aus JS (`opts`), Bearer-Auth, ETag/If-None-Match, Retry mit Backoff.
|
||||
//
|
||||
// Nach erfolgreichem Sync wird — wenn Family Controls authorisiert ist —
|
||||
// `applyWebContentLayer()` erneut aufgerufen, damit die neue Liste sofort
|
||||
// greift. Solange der Backend-Endpoint nicht deployed ist, schlägt der
|
||||
// Fetch fehl → `loadWebContentDomains` fällt sauber auf die gebündelte
|
||||
// JSON zurück (App funktioniert offline weiter).
|
||||
|
||||
AsyncFunction("syncWebContentDomains") { (opts: [String: String]) async throws -> [String: Any] in
|
||||
guard let baseURL = opts["baseURL"], let authToken = opts["authToken"] else {
|
||||
throw NSError(
|
||||
domain: "RebreakProtection", code: 400,
|
||||
userInfo: [NSLocalizedDescriptionKey: "missing baseURL or authToken"]
|
||||
)
|
||||
}
|
||||
guard let endpoint = URL(string: "\(baseURL)/api/protection/webcontent-domains") else {
|
||||
throw NSError(
|
||||
domain: "RebreakProtection", code: 400,
|
||||
userInfo: [NSLocalizedDescriptionKey: "invalid baseURL"]
|
||||
)
|
||||
}
|
||||
|
||||
// Retry mit Backoff (1s, 2s) — fängt transiente Netzwerk-Fehler.
|
||||
let maxAttempts = 3
|
||||
var attempt = 1
|
||||
var lastError: Error?
|
||||
|
||||
while attempt <= maxAttempts {
|
||||
do {
|
||||
var request = URLRequest(url: endpoint)
|
||||
request.setValue("Bearer \(authToken)", forHTTPHeaderField: "Authorization")
|
||||
if let lastEtag = UserDefaults(suiteName: APP_GROUP)?.string(forKey: WEBCONTENT_ETAG_KEY) {
|
||||
request.setValue(lastEtag, forHTTPHeaderField: "If-None-Match")
|
||||
}
|
||||
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
throw NSError(
|
||||
domain: "RebreakProtection", code: 500,
|
||||
userInfo: [NSLocalizedDescriptionKey: "invalid response"]
|
||||
)
|
||||
}
|
||||
|
||||
if httpResponse.statusCode == 304 {
|
||||
SharedLogStore.append("📡 [webContent] sync 304 (cached)")
|
||||
return ["updated": false]
|
||||
}
|
||||
|
||||
guard httpResponse.statusCode == 200 else {
|
||||
throw NSError(
|
||||
domain: "RebreakProtection", code: httpResponse.statusCode,
|
||||
userInfo: [NSLocalizedDescriptionKey: "HTTP \(httpResponse.statusCode)"]
|
||||
)
|
||||
}
|
||||
|
||||
// Vor dem Schreiben validieren: muss parsebares JSON mit Country-Keys
|
||||
// sein (gleiche Struktur wie die gebündelte gambling-domains.json).
|
||||
// Eine kaputte Response NICHT in den Cache schreiben — sonst killt
|
||||
// sie den Fallback.
|
||||
guard let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
||||
throw NSError(
|
||||
domain: "RebreakProtection", code: 422,
|
||||
userInfo: [NSLocalizedDescriptionKey: "response is not valid JSON object"]
|
||||
)
|
||||
}
|
||||
// Mindestens ein Country-Key mit einem String-Array erwartet.
|
||||
let countryKeyCount = root.keys.filter { !$0.hasPrefix("_") }.count
|
||||
guard countryKeyCount > 0 else {
|
||||
throw NSError(
|
||||
domain: "RebreakProtection", code: 422,
|
||||
userInfo: [NSLocalizedDescriptionKey: "response has no country keys"]
|
||||
)
|
||||
}
|
||||
|
||||
guard let appGroupURL = FileManager.default
|
||||
.containerURL(forSecurityApplicationGroupIdentifier: APP_GROUP)
|
||||
else {
|
||||
throw NSError(
|
||||
domain: "RebreakProtection", code: 500,
|
||||
userInfo: [NSLocalizedDescriptionKey: "App-Group container unavailable"]
|
||||
)
|
||||
}
|
||||
|
||||
let finalURL = appGroupURL.appendingPathComponent(WEBCONTENT_CACHE_FILENAME)
|
||||
let tmpURL = finalURL.appendingPathExtension("tmp")
|
||||
try data.write(to: tmpURL, options: .atomic)
|
||||
|
||||
// DiGA-Hardening (gespiegelt von syncBlocklist).
|
||||
try? (tmpURL as NSURL).setResourceValue(
|
||||
URLFileProtection.complete,
|
||||
forKey: .fileProtectionKey
|
||||
)
|
||||
var mut = tmpURL
|
||||
var rv = URLResourceValues()
|
||||
rv.isExcludedFromBackup = true
|
||||
try? mut.setResourceValues(rv)
|
||||
|
||||
// Atomic replace
|
||||
_ = try? FileManager.default.removeItem(at: finalURL)
|
||||
try FileManager.default.moveItem(at: tmpURL, to: finalURL)
|
||||
|
||||
// ETag + lastSync persistieren
|
||||
let defaults = UserDefaults(suiteName: APP_GROUP)
|
||||
if let newEtag = httpResponse.value(forHTTPHeaderField: "etag") {
|
||||
defaults?.set(newEtag, forKey: WEBCONTENT_ETAG_KEY)
|
||||
}
|
||||
defaults?.set(ISO8601DateFormatter().string(from: Date()), forKey: WEBCONTENT_LAST_SYNC_KEY)
|
||||
|
||||
let version = ((root["_meta"] as? [String: Any])?["version"] as? Int) ?? 0
|
||||
SharedLogStore.append(
|
||||
"📡 [webContent] sync ok: \(countryKeyCount) Länder (version=\(version))"
|
||||
)
|
||||
|
||||
// Reapply: wenn Family Controls authorisiert ist, die neue Liste
|
||||
// sofort wirksam machen. Best-effort — ein Fehlschlag hier kippt den
|
||||
// erfolgreichen Sync NICHT.
|
||||
var reapplied = false
|
||||
if #available(iOS 16.0, *),
|
||||
AuthorizationCenter.shared.authorizationStatus == .approved {
|
||||
let wc = Self.applyWebContentLayer()
|
||||
reapplied = wc.enabled
|
||||
SharedLogStore.append(
|
||||
"🛡️ [webContent] reapply nach sync: enabled=\(wc.enabled) count=\(wc.count) region=\(wc.region)"
|
||||
)
|
||||
}
|
||||
|
||||
return [
|
||||
"updated": true,
|
||||
"countries": countryKeyCount,
|
||||
"version": version,
|
||||
"reapplied": reapplied,
|
||||
]
|
||||
|
||||
} catch {
|
||||
lastError = error
|
||||
let nsError = error as NSError
|
||||
let isTransient = nsError.domain == NSURLErrorDomain && (
|
||||
nsError.code == NSURLErrorNotConnectedToInternet ||
|
||||
nsError.code == NSURLErrorCannotConnectToHost ||
|
||||
nsError.code == NSURLErrorTimedOut ||
|
||||
nsError.code == NSURLErrorNetworkConnectionLost ||
|
||||
nsError.code == NSURLErrorCannotFindHost
|
||||
)
|
||||
if isTransient && attempt < maxAttempts {
|
||||
let delaySec = UInt64(attempt)
|
||||
SharedLogStore.append("⏳ [webContent] sync transient \(nsError.code), retry in \(delaySec)s")
|
||||
try? await Task.sleep(nanoseconds: delaySec * 1_000_000_000)
|
||||
attempt += 1
|
||||
continue
|
||||
}
|
||||
SharedLogStore.append("❌ [webContent] sync failed: \(error.localizedDescription)")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError ?? NSError(
|
||||
domain: "RebreakProtection", code: 500,
|
||||
userInfo: [NSLocalizedDescriptionKey: "max attempts exhausted"]
|
||||
)
|
||||
}
|
||||
|
||||
// ───────── runHealthProbe: hidden WKWebView gegen bet365 ─────────
|
||||
|
||||
AsyncFunction("runHealthProbe") { (opts: [String: Any]?) async -> [String: Any] in
|
||||
@ -820,16 +996,49 @@ public class RebreakProtectionModule: Module {
|
||||
return (true, webDomains.count, resolvedRegion)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// Lädt die kuratierte Gambling-Domain-Liste für ein Land — **cache-first**.
|
||||
///
|
||||
/// 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
|
||||
/// 1. Zuerst die per `syncWebContentDomains` vom Backend geholte
|
||||
/// `webcontent-domains.json` aus dem App-Group-Container lesen.
|
||||
/// 2. Nur wenn die fehlt / nicht parsebar ist / das Land nicht enthält →
|
||||
/// Fallback auf die gebündelte `gambling-domains.json` im
|
||||
/// RebreakProtectionResources.bundle (Offline-Seed).
|
||||
///
|
||||
/// Beide Quellen haben dieselbe Struktur (`{ "DE": ["..."], ... }` plus
|
||||
/// `_comment`/`_meta`-Felder, die ignoriert werden) und werden über
|
||||
/// `parseDomainsJSON` identisch geparst. Liefert [] wenn keine der beiden
|
||||
/// Quellen das Land listet — 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.
|
||||
// ── 1) Cache-first: App-Group-webcontent-domains.json ──────────────────
|
||||
if let appGroupURL = FileManager.default
|
||||
.containerURL(forSecurityApplicationGroupIdentifier: APP_GROUP) {
|
||||
let cacheURL = appGroupURL.appendingPathComponent(WEBCONTENT_CACHE_FILENAME)
|
||||
if let domains = parseDomainsJSON(at: cacheURL, forRegion: region) {
|
||||
SharedLogStore.append(
|
||||
"🛡️ [loadWebContentDomains] cache-hit (\(domains.count) Domains, \(region.uppercased()))")
|
||||
return domains
|
||||
}
|
||||
}
|
||||
|
||||
// ── 2) Fallback: gebündelte gambling-domains.json (Offline-Seed) ───────
|
||||
if let url = bundledWebContentDomainsURL(),
|
||||
let domains = parseDomainsJSON(at: url, forRegion: region) {
|
||||
SharedLogStore.append(
|
||||
"🛡️ [loadWebContentDomains] bundle-fallback (\(domains.count) Domains, \(region.uppercased()))")
|
||||
return domains
|
||||
}
|
||||
|
||||
SharedLogStore.append(
|
||||
"❌ [loadWebContentDomains] keine Domains für \(region.uppercased()) (cache+bundle leer)")
|
||||
return []
|
||||
}
|
||||
|
||||
/// Löst die URL der gebündelten `gambling-domains.json` auf. Das JSON wird
|
||||
/// von der Podspec als RebreakProtectionResources.bundle ins App-Bundle
|
||||
/// gepackt; je nach Build-Konfig liegt es im Pod-Bundle oder flach im
|
||||
/// Main-Bundle.
|
||||
private static func bundledWebContentDomainsURL() -> URL? {
|
||||
let moduleBundle = Bundle(for: RebreakProtectionModule.self)
|
||||
var jsonURL: URL? = moduleBundle.url(
|
||||
forResource: WEBCONTENT_DOMAINS_FILE, withExtension: "json")
|
||||
@ -846,20 +1055,24 @@ public class RebreakProtectionModule: Module {
|
||||
jsonURL = Bundle.main.url(
|
||||
forResource: WEBCONTENT_DOMAINS_FILE, withExtension: "json")
|
||||
}
|
||||
return jsonURL
|
||||
}
|
||||
|
||||
guard let url = jsonURL,
|
||||
/// Parst eine Domain-Liste-JSON-Datei (Cache ODER Bundle — identische
|
||||
/// Struktur) und gibt die normalisierte Domain-Liste für `region` zurück.
|
||||
/// Liefert `nil` wenn die Datei fehlt, nicht parsebar ist oder das Land
|
||||
/// nicht enthält — der Aufrufer behandelt `nil` als „diese Quelle leer,
|
||||
/// nächste versuchen".
|
||||
private static func parseDomainsJSON(at url: URL, forRegion region: String) -> [String]? {
|
||||
guard FileManager.default.fileExists(atPath: url.path),
|
||||
let data = try? Data(contentsOf: url),
|
||||
let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
||||
let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let domains = root[region.uppercased()] as? [String]
|
||||
else {
|
||||
SharedLogStore.append("❌ [loadWebContentDomains] \(WEBCONTENT_DOMAINS_FILE).json nicht ladbar")
|
||||
return []
|
||||
}
|
||||
|
||||
guard let domains = root[region.uppercased()] as? [String] else {
|
||||
return []
|
||||
return nil
|
||||
}
|
||||
// Defensiv normalisieren: leere Strings raus, lowercase, dedupliziert,
|
||||
// hart auf das Apple-Limit gekappt.
|
||||
// hart auf das Apple-Limit (WEBCONTENT_MAX_DOMAINS) gekappt.
|
||||
var seen = Set<String>()
|
||||
var cleaned: [String] = []
|
||||
for raw in domains {
|
||||
@ -869,6 +1082,8 @@ public class RebreakProtectionModule: Module {
|
||||
cleaned.append(d)
|
||||
if cleaned.count >= WEBCONTENT_MAX_DOMAINS { break }
|
||||
}
|
||||
// Ein leeres Array ist eine gültige Antwort dieser Quelle (Land gelistet,
|
||||
// aber 0 Domains) — der Aufrufer entscheidet dann no_domains_for_region.
|
||||
return cleaned
|
||||
}
|
||||
|
||||
|
||||
@ -74,6 +74,30 @@ export type SyncBlocklistResult = {
|
||||
plan?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Opts für syncWebContentDomains (iOS Layer 2). Gleiche Shape wie
|
||||
* SyncBlocklistOpts — baseURL + authToken kommen aus der Supabase-Session.
|
||||
*/
|
||||
export type SyncWebContentDomainsOpts = {
|
||||
baseURL: string;
|
||||
authToken: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Ergebnis von syncWebContentDomains (iOS Layer 2).
|
||||
* `updated` = neue Liste wurde heruntergeladen + gecached (false bei 304).
|
||||
* `countries` = Anzahl Country-Keys in der Response.
|
||||
* `version` = `_meta.version` der Backend-Response.
|
||||
* `reapplied` = applyWebContentLayer() wurde nach dem Sync erneut ausgeführt
|
||||
* (nur wenn Family Controls authorisiert ist).
|
||||
*/
|
||||
export type SyncWebContentDomainsResult = {
|
||||
updated: boolean;
|
||||
countries?: number;
|
||||
version?: number;
|
||||
reapplied?: boolean;
|
||||
};
|
||||
|
||||
export type HealthProbeOutcome = "blocked" | "loaded" | "offline" | "timeout";
|
||||
|
||||
export type HealthProbeOpts = {
|
||||
|
||||
@ -9,6 +9,8 @@ import type {
|
||||
RebreakProtectionEvents,
|
||||
SyncBlocklistOpts,
|
||||
SyncBlocklistResult,
|
||||
SyncWebContentDomainsOpts,
|
||||
SyncWebContentDomainsResult,
|
||||
SystemSettingsTarget,
|
||||
WebContentFilterResult,
|
||||
} from './RebreakProtection.types';
|
||||
@ -76,6 +78,23 @@ declare class RebreakProtectionModule extends NativeModule<RebreakProtectionEven
|
||||
*/
|
||||
clearWebContentFilter(): Promise<{ cleared: boolean; error?: string }>;
|
||||
|
||||
/**
|
||||
* iOS Layer 2 — synct die kuratierte Gambling-Domain-Liste vom Backend
|
||||
* (`GET /api/protection/webcontent-domains`) und cached sie als
|
||||
* `webcontent-domains.json` im App-Group-Container. `loadWebContentDomains`
|
||||
* liest danach cache-first; die gebündelte `gambling-domains.json` bleibt
|
||||
* nur noch Offline-Seed/Fallback.
|
||||
*
|
||||
* Gespiegelt von `syncBlocklist`: baseURL + authToken aus der Supabase-
|
||||
* Session, Bearer-Auth, ETag/If-None-Match, Retry mit Backoff. Nach
|
||||
* erfolgreichem Sync wird — wenn Family Controls authorisiert ist —
|
||||
* `applyWebContentLayer()` erneut ausgeführt, damit die neue Liste sofort
|
||||
* greift. Server respondet 304 wenn ETag matched → updated=false.
|
||||
*/
|
||||
syncWebContentDomains(
|
||||
opts: SyncWebContentDomainsOpts,
|
||||
): Promise<SyncWebContentDomainsResult>;
|
||||
|
||||
/** Aktueller Device-State. Polling- und Health-Check-Pfad. */
|
||||
getDeviceState(): Promise<DeviceLayers>;
|
||||
|
||||
|
||||
@ -35,6 +35,10 @@ class RebreakProtectionModuleWeb extends NativeModule<RebreakProtectionEvents> {
|
||||
return { cleared: false, error: 'web_stub' };
|
||||
}
|
||||
|
||||
async syncWebContentDomains() {
|
||||
return { updated: false };
|
||||
}
|
||||
|
||||
async syncBlocklist(): Promise<SyncBlocklistResult> {
|
||||
return { updated: false, count: 0 };
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user