/* eslint-disable @typescript-eslint/no-var-requires */ /** * Expo Config-Plugin — bindet das NEURLFilter-ExtensionKit-Target * (`RebreakURLFilterExtension`) beim Prebuild ins iOS-Projekt ein. * * Ersetzt das frühere NEFilter-Plugin. Erkenntnis aus Apples `SimpleURLFilter`- * Sample: das Target ist KEIN exotischer Produkttyp — es bleibt das klassische * `app_extension`. Die ExtensionKit-Natur kommt aus: * 1. der Info.plist (`EXAppExtensionAttributes` / `EXExtensionPointIdentifier`) * 2. der Embed-Phase „Embed Foundation Extensions" mit `dstSubfolderSpec = 16` * (das klassische „Embed App Extensions" wäre 13). * * Was es macht: * 1) Haupt-App-Entitlements (url-filter-provider, family-controls, app-groups) * 2) kopiert `modules/rebreak-protection/ios/RebreakURLFilterExtension/` → `ios/` * 3) fügt das Xcode-Target `RebreakURLFilterExtension` hinzu * * Idempotent — kann beliebig oft via `expo prebuild` laufen. */ const fs = require('fs'); const path = require('path'); const { withEntitlementsPlist, withDangerousMod, withXcodeProject, } = require('@expo/config-plugins'); const APP_GROUP = 'group.org.rebreak.app'; const TARGET_NAME = 'RebreakURLFilterExtension'; const EXT_BUNDLE_SUFFIX = 'URLFilterExtension'; // → org.rebreak.app.URLFilterExtension const DEVELOPMENT_TEAM = '84BQ7MTFYK'; const MODULE_DIR = path.join( __dirname, '..', 'modules', 'rebreak-protection', 'ios', TARGET_NAME, ); // Swift-Quellen (Apple-Bloom vendored + unser Control-Provider). const SWIFT_SOURCES = [ 'BloomFilter.swift', 'Murmur3Hash.swift', 'FNV1aHash.swift', 'RebreakURLFilterControlProvider.swift', ]; // Bundle-Resources. const RESOURCES = ['bloom_filter.plist']; // ─── 1) Haupt-App-Entitlements ────────────────────────────────────────────── function withMainAppEntitlements(config) { return withEntitlementsPlist(config, (cfg) => { // NEURLFilter-Entitlement (ersetzt das alte content-filter-provider). // Dev-Builds laufen ohne Apple-Freigabe; TestFlight/Store brauchen die // genehmigte Capability (Developer-Portal → Capability Requests). cfg.modResults['com.apple.developer.networking.networkextension'] = [ 'url-filter-provider', ]; // Family Controls = Kern-Funktion, Apple-Distribution-Entitlement freigegeben // (v0.3.4) → DEFAULT AN. Nur ein explizites REBREAK_ENABLE_FAMILY_CONTROLS=0 // schaltet es ab (Escape-Hatch). Muss identisch zu app.config.ts // `familyControlsEnabled` bleiben. if (process.env.REBREAK_ENABLE_FAMILY_CONTROLS !== '0') { cfg.modResults['com.apple.developer.family-controls'] = true; } const groups = cfg.modResults['com.apple.security.application-groups'] || []; if (!groups.includes(APP_GROUP)) { cfg.modResults['com.apple.security.application-groups'] = [...groups, APP_GROUP]; } return cfg; }); } // ─── 2) Extension-Sources ins ios/-Verzeichnis kopieren ───────────────────── function withCopyExtensionSources(config) { return withDangerousMod(config, [ 'ios', async (cfg) => { const dest = path.join(cfg.modRequest.platformProjectRoot, TARGET_NAME); if (!fs.existsSync(MODULE_DIR)) { throw new Error( `[with-rebreak-protection-ios] Extension source dir missing: ${MODULE_DIR}`, ); } if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true }); for (const file of fs.readdirSync(MODULE_DIR)) { fs.copyFileSync(path.join(MODULE_DIR, file), path.join(dest, file)); } return cfg; }, ]); } // ─── 3) Xcode-Target hinzufügen ────────────────────────────────────────────── function withExtensionTarget(config) { return withXcodeProject(config, async (cfg) => { const proj = cfg.modResults; // Idempotenz: skip wenn Target schon angelegt. if (proj.pbxTargetByName(TARGET_NAME)) { return cfg; } const mainBundleId = cfg.ios?.bundleIdentifier; if (!mainBundleId) { throw new Error('[with-rebreak-protection-ios] ios.bundleIdentifier fehlt in app.config'); } const extBundleId = `${mainBundleId}.${EXT_BUNDLE_SUFFIX}`; // ── Target anlegen (Produkttyp app_extension — wie Apples SimpleURLFilter) ── const target = proj.addTarget(TARGET_NAME, 'app_extension', TARGET_NAME, extBundleId); // ── Build-Phasen ── proj.addBuildPhase(SWIFT_SOURCES, 'PBXSourcesBuildPhase', 'Sources', target.uuid); proj.addBuildPhase(RESOURCES, 'PBXResourcesBuildPhase', 'Resources', target.uuid); proj.addBuildPhase( ['NetworkExtension.framework'], 'PBXFrameworksBuildPhase', 'Frameworks', target.uuid, ); // ── PBXGroup für die Extension-Dateien ── const pbxGroup = proj.addPbxGroup( [...SWIFT_SOURCES, ...RESOURCES, 'Info.plist', `${TARGET_NAME}.entitlements`], TARGET_NAME, TARGET_NAME, ); const groups = proj.hash.project.objects.PBXGroup; Object.keys(groups).forEach((key) => { if ( groups[key].name === 'CustomTemplate' || (groups[key].name === undefined && groups[key].path === undefined) ) { proj.addToPbxGroup(pbxGroup.uuid, key); } }); // ── Build-Settings auf der Target-Configuration ── const configurations = proj.pbxXCBuildConfigurationSection(); Object.keys(configurations) .filter((k) => typeof configurations[k] === 'object') .forEach((k) => { const buildSettingsObj = configurations[k].buildSettings; if ( buildSettingsObj && buildSettingsObj.PRODUCT_NAME && buildSettingsObj.PRODUCT_NAME.replace(/"/g, '') === TARGET_NAME ) { buildSettingsObj.INFOPLIST_FILE = `"${TARGET_NAME}/Info.plist"`; buildSettingsObj.CODE_SIGN_ENTITLEMENTS = `"${TARGET_NAME}/${TARGET_NAME}.entitlements"`; // NEURLFilter ist iOS 26+. Die Extension lädt nur dort — höheres // Deployment-Target als die Haupt-App (iOS 16+) ist korrekt. buildSettingsObj.IPHONEOS_DEPLOYMENT_TARGET = '26.0'; buildSettingsObj.SWIFT_VERSION = '5.0'; buildSettingsObj.TARGETED_DEVICE_FAMILY = '"1,2"'; buildSettingsObj.CODE_SIGN_STYLE = 'Automatic'; // EAS managed credentials setzen DEVELOPMENT_TEAM nur auf der Main-App // → Extension braucht expliziten Team-Wert. buildSettingsObj.DEVELOPMENT_TEAM = DEVELOPMENT_TEAM; } }); // ── Embed-Phase auf ExtensionKit umstellen ── // proj.addTarget() hat bereits eine „Copy Files"-PBXCopyFilesBuildPhase // (dstSubfolderSpec 13 = PlugIns/) im Haupt-Target angelegt, die unsere // .appex einbettet. iOS' Installer (MIPluginKitBundle) behandelt aber // ALLES in PlugIns/ als klassische PluginKit-Extension und verlangt ein // NSExtension-Dict im Info.plist — das ExtensionKit-Extensions NICHT haben // ("AppexBundleMissingNSExtensionDict", MIInstallerError 39). ExtensionKit- // Extensions MÜSSEN nach Extensions/. Also die bestehende Phase umbiegen: // dstSubfolderSpec 16 + dstPath $(EXTENSIONS_FOLDER_PATH) — exakt wie // Apples SimpleURLFilter.xcodeproj. KEINE zweite Phase anlegen (sonst wird // die .appex doppelt eingebettet). const mainTargetUuid = proj.getFirstTarget().uuid; const copyFilesPhases = proj.hash.project.objects.PBXCopyFilesBuildPhase || {}; let embedFixed = false; Object.keys(copyFilesPhases).forEach((key) => { const phase = copyFilesPhases[key]; if (typeof phase !== 'object') return; const embedsOurAppex = (phase.files || []).some( (f) => typeof f?.comment === 'string' && f.comment.includes(`${TARGET_NAME}.appex`), ); if (!embedsOurAppex) return; phase.name = '"Embed Foundation Extensions"'; phase.dstSubfolderSpec = 16; phase.dstPath = '"$(EXTENSIONS_FOLDER_PATH)"'; embedFixed = true; }); if (!embedFixed) { throw new Error( '[with-rebreak-protection-ios] Embed-CopyFiles-Phase für die .appex nicht gefunden', ); } // ── Target-Dependency: Haupt-App baut die Extension vorher ── proj.addTargetDependency(mainTargetUuid, [target.uuid]); return cfg; }); } // ─── Composition ──────────────────────────────────────────────────────────── module.exports = function withRebreakProtectionIos(config) { config = withMainAppEntitlements(config); config = withCopyExtensionSources(config); config = withExtensionTarget(config); return config; };