/* 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']; // ─── Packet-Tunnel-Extension (Layer 1 — Default-Filter) ────────────────────── // Neues Target: der NEPacketTunnelProvider-DNS-Sinkhole. Ersetzt NEURLFilter // als primären, lieferbaren iOS-Filter. KLASSISCHE PluginKit-App-Extension — // normaler `app_extension`-Pfad, normale „Embed App Extensions"-Phase // (dstSubfolderSpec 13). KEIN ExtensionKit-Sonderweg. const PT_TARGET_NAME = 'RebreakPacketTunnelExtension'; const PT_BUNDLE_SUFFIX = 'PacketTunnelExtension'; // → org.rebreak.app.PacketTunnelExtension const PT_MODULE_DIR = path.join( __dirname, '..', 'modules', 'rebreak-protection', 'ios', PT_TARGET_NAME, ); // Swift-Quellen (Provider + Filter-Logik, aus Android nach Swift portiert). const PT_SWIFT_SOURCES = [ 'PacketTunnelProvider.swift', 'DnsFilter.swift', 'HashList.swift', 'DomainHasher.swift', ]; // ─── Content-Filter-Extension (Layer 1 — Supervised-Pfad) ──────────────────── // Klassischer NEFilterDataProvider — wiederbelebt aus a80cc8b. Wird auf // supervised-MDM-Geräten als Layer 1 statt PacketTunnel aktiviert (kein VPN- // Toggle für den User). Dynamische Hash-Liste aus App-Group blocklist.bin. // KEIN PIR-Server, KEIN ExtensionKit-Sonderweg — klassische dst-13-PlugIns/. const CF_TARGET_NAME = 'RebreakContentFilter'; const CF_BUNDLE_SUFFIX = 'ContentFilterExtension'; // → org.rebreak.app.ContentFilterExtension const CF_MODULE_DIR = path.join( __dirname, '..', 'modules', 'rebreak-protection', 'ios', CF_TARGET_NAME, ); const CF_SWIFT_SOURCES = ['FilterDataProvider.swift']; // ─── 1) Haupt-App-Entitlements ────────────────────────────────────────────── function withMainAppEntitlements(config) { return withEntitlementsPlist(config, (cfg) => { // NetworkExtension-Entitlement. // - packet-tunnel-provider: PacketTunnel-DNS-Sinkhole (unsupervised-Pfad, // User-toggleable VPN) // - content-filter-provider: klassisches NEFilter (supervised-Pfad, kein // User-Toggle in Settings) // App entscheidet zur Laufzeit per isMdmSupervised welcher Stack aktiviert // wird — beide Entitlements müssen verfügbar sein. cfg.modResults['com.apple.developer.networking.networkextension'] = [ 'packet-tunnel-provider', 'content-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) => { // Extension-Verzeichnis(se) nach ios/ kopieren. for (const [target, srcDir] of [ [PT_TARGET_NAME, PT_MODULE_DIR], [CF_TARGET_NAME, CF_MODULE_DIR], ]) { const dest = path.join(cfg.modRequest.platformProjectRoot, target); if (!fs.existsSync(srcDir)) { throw new Error( `[with-rebreak-protection-ios] Extension source dir missing: ${srcDir}`, ); } if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true }); for (const file of fs.readdirSync(srcDir)) { fs.copyFileSync(path.join(srcDir, 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; }); } // ─── 4) Packet-Tunnel-Xcode-Target hinzufügen ──────────────────────────────── // // KLASSISCHE App-Extension — anders als das NEURLFilter-Target: // - normaler `app_extension`-Produkttyp, // - „Embed App Extensions"-Phase mit dstSubfolderSpec 13 (PlugIns/), // - KEIN ExtensionKit-Sonderweg (kein dstSubfolderSpec 16, kein // EXAppExtensionAttributes). // - Deployment-Target = Main-App (iOS 16), KEIN 26.0-Gate. // // ACHTUNG: `addTarget` legt zwar eine korrekte dst-13-Phase an, embedded die // .appex aber wegen einer Eigenheit der `xcode`-Lib in die NEURLFilter-dst-16- // Phase (siehe ausführliche Begründung am Embed-Phase-Fix unten). Darum MUSS // die .appex hier explizit in die eigene dst-13-Phase umgehängt werden. function withPacketTunnelTarget(config) { return withXcodeProject(config, async (cfg) => { const proj = cfg.modResults; // Idempotenz: skip wenn Target schon angelegt. if (proj.pbxTargetByName(PT_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}.${PT_BUNDLE_SUFFIX}`; // ── Target anlegen (klassischer app_extension-Produkttyp) ── const target = proj.addTarget( PT_TARGET_NAME, 'app_extension', PT_TARGET_NAME, extBundleId, ); // ── Build-Phasen ── proj.addBuildPhase( PT_SWIFT_SOURCES, 'PBXSourcesBuildPhase', 'Sources', target.uuid, ); proj.addBuildPhase( ['NetworkExtension.framework'], 'PBXFrameworksBuildPhase', 'Frameworks', target.uuid, ); // ── PBXGroup für die Extension-Dateien ── const pbxGroup = proj.addPbxGroup( [...PT_SWIFT_SOURCES, 'Info.plist', `${PT_TARGET_NAME}.entitlements`], PT_TARGET_NAME, PT_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, '') === PT_TARGET_NAME ) { buildSettingsObj.INFOPLIST_FILE = `"${PT_TARGET_NAME}/Info.plist"`; buildSettingsObj.CODE_SIGN_ENTITLEMENTS = `"${PT_TARGET_NAME}/${PT_TARGET_NAME}.entitlements"`; // Packet-Tunnel läuft ab iOS 16 — Deployment-Target = Main-App. buildSettingsObj.IPHONEOS_DEPLOYMENT_TARGET = '16.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 korrigieren — die .appex MUSS allein in dst 13 (PlugIns/) ── // // BUG (verifiziert gegen node_modules/xcode/lib/pbxProject.js + einen // End-to-End-Lauf der echten Plugin-Kette): // // 1. `withXcodeProject`-Mods laufen LIFO — `withPacketTunnelTarget` ist // zuletzt registriert und läuft daher ZUERST, `withExtensionTarget` // (NEURLFilter) DANACH. // 2. `proj.addTarget('app_extension')` ruft intern (via `addProductFile`) // `addToPbxCopyfilesBuildPhase`, BEVOR die neue dst-13-Copy-Files-Phase // existiert. `buildPhaseObject` findet im neuen Target keine eigene // Phase und fällt auf die ERSTE PBXCopyFilesBuildPhase zurück, deren // SECTION-COMMENT exakt „Copy Files" ist — projektweit. // 3. Ergebnis ohne Fix: die NEURLFilter-`addTarget` (läuft als zweites) // biegt seine Phase per `phase.name`/`dstSubfolderSpec` auf dst 16 um, // lässt den SECTION-COMMENT aber auf „Copy Files". Beim PacketTunnel- // `addTarget` matcht `buildPhaseObject` diesen Comment und pusht die // PacketTunnel-.appex in dieselbe dst-16-Phase. → .appex landet in // `ReBreak.app/Extensions/` statt `PlugIns/` → Install-Crash // `AppexBundleMissingEXAppExtensionAttributesDict` (klassische // NSExtension im ExtensionKit-Ordner). // // Fix (zwei Teile, beide nötig): // (a) Die PacketTunnel-.appex aus JEDER Copy-Files-Phase herausziehen und // exklusiv in die eigene, von `addTarget` angelegte dst-13-Phase legen. // (b) Den SECTION-COMMENT dieser dst-13-Phase von „Copy Files" auf // „Embed App Extensions" umbenennen. SONST matcht der NACH uns laufende // `withExtensionTarget`-`addTarget`-Aufruf via `buildPhaseObject( // 'Copy Files')` unsere PacketTunnel-Phase und der dortige dst-16-Umbau // zieht die PacketTunnel-.appex erneut mit. Comment-Rename entkoppelt // beide Phasen sauber (NEURLFilters eigener Umbau matcht per // .appex-Dateiname, nicht per Comment — bleibt also korrekt). const ptCopyFilesPhases = proj.hash.project.objects.PBXCopyFilesBuildPhase || {}; const ptPhaseKeys = Object.keys(ptCopyFilesPhases).filter( (k) => typeof ptCopyFilesPhases[k] === 'object' && !/_comment$/.test(k), ); // (a) Den PacketTunnel-.appex-BuildFile-Eintrag finden und aus ALLEN // Copy-Files-Phasen entfernen. let ptAppexBuildFile = null; ptPhaseKeys.forEach((key) => { const phase = ptCopyFilesPhases[key]; const kept = []; (phase.files || []).forEach((f) => { if ( typeof f?.comment === 'string' && f.comment.includes(`${PT_TARGET_NAME}.appex`) ) { ptAppexBuildFile = f; } else { kept.push(f); } }); phase.files = kept; }); if (!ptAppexBuildFile) { throw new Error( '[with-rebreak-protection-ios] PacketTunnel-.appex-BuildFile nicht gefunden', ); } // (b) Die von addTarget frisch angelegte (jetzt leere) dst-13-Phase finden, // die .appex exklusiv dort einhängen UND den Section-Comment umbenennen. // dst 13 = PlugIns/ = der korrekte Ort für eine klassische NSExtension- // PluginKit-Extension. const ptDst13Key = ptPhaseKeys.find( (k) => ptCopyFilesPhases[k].dstSubfolderSpec === 13 && (ptCopyFilesPhases[k].files || []).length === 0, ); if (!ptDst13Key) { throw new Error( '[with-rebreak-protection-ios] keine leere dst-13-Copy-Files-Phase für die PacketTunnel-.appex gefunden', ); } ptCopyFilesPhases[ptDst13Key].name = '"Embed App Extensions"'; ptCopyFilesPhases[ptDst13Key].files = [ptAppexBuildFile]; // Section-Comment-Key umbenennen — entkoppelt die Phase vom // `buildPhaseObject('Copy Files')`-Lookup des NEURLFilter-Targets. const ptDst13CommentKey = `${ptDst13Key}_comment`; if (ptCopyFilesPhases[ptDst13CommentKey] === 'Copy Files') { ptCopyFilesPhases[ptDst13CommentKey] = 'Embed App Extensions'; } // Auch den Build-Phase-Verweis im Target auf den neuen Comment ziehen, // damit pbxproj-Serialisierung + Xcode-Anzeige konsistent bleiben. const ptNativeTargets = proj.hash.project.objects.PBXNativeTarget || {}; Object.keys(ptNativeTargets).forEach((tk) => { const nt = ptNativeTargets[tk]; if (typeof nt !== 'object' || !Array.isArray(nt.buildPhases)) return; nt.buildPhases.forEach((bp) => { if (bp.value === ptDst13Key && bp.comment === 'Copy Files') { bp.comment = 'Embed App Extensions'; } }); }); // ── Target-Dependency: Haupt-App baut die Extension vorher ── const mainTargetUuid = proj.getFirstTarget().uuid; proj.addTargetDependency(mainTargetUuid, [target.uuid]); return cfg; }); } // ─── 5) Content-Filter-Xcode-Target hinzufügen ─────────────────────────────── // // 1:1 zum PacketTunnel-Hook: klassische app_extension, dst-13 PlugIns/, // derselbe Embed-Phase-Fix (Comment-Rename, exklusive .appex-Zuweisung). // LIFO-Composition: dieser Hook läuft NACH PacketTunnel — wenn PT seine Phase // bereits umbenannt hat ("Embed App Extensions"), findet `addProductFile` keine // "Copy Files"-Phase mehr und legt eine frische dst-13 für CF an, die wir hier // dann gleich richtig setzen. function withContentFilterTarget(config) { return withXcodeProject(config, async (cfg) => { const proj = cfg.modResults; if (proj.pbxTargetByName(CF_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}.${CF_BUNDLE_SUFFIX}`; const target = proj.addTarget( CF_TARGET_NAME, 'app_extension', CF_TARGET_NAME, extBundleId, ); proj.addBuildPhase( CF_SWIFT_SOURCES, 'PBXSourcesBuildPhase', 'Sources', target.uuid, ); proj.addBuildPhase( ['NetworkExtension.framework'], 'PBXFrameworksBuildPhase', 'Frameworks', target.uuid, ); const pbxGroup = proj.addPbxGroup( [...CF_SWIFT_SOURCES, 'Info.plist', `${CF_TARGET_NAME}.entitlements`], CF_TARGET_NAME, CF_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); } }); 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, '') === CF_TARGET_NAME ) { buildSettingsObj.INFOPLIST_FILE = `"${CF_TARGET_NAME}/Info.plist"`; buildSettingsObj.CODE_SIGN_ENTITLEMENTS = `"${CF_TARGET_NAME}/${CF_TARGET_NAME}.entitlements"`; // NEFilterDataProvider gibt es seit iOS 9 — Deployment-Target = Main-App. buildSettingsObj.IPHONEOS_DEPLOYMENT_TARGET = '16.0'; buildSettingsObj.SWIFT_VERSION = '5.0'; buildSettingsObj.TARGETED_DEVICE_FAMILY = '"1,2"'; buildSettingsObj.CODE_SIGN_STYLE = 'Automatic'; buildSettingsObj.DEVELOPMENT_TEAM = DEVELOPMENT_TEAM; } }); // Embed-Phase-Fix (siehe ausführliche Begründung im PacketTunnel-Hook). // .appex aus allen Copy-Files-Phasen ziehen und exklusiv in eigene dst-13 // legen + Section-Comment auf "Embed App Extensions" umbenennen. const cfCopyFilesPhases = proj.hash.project.objects.PBXCopyFilesBuildPhase || {}; const cfPhaseKeys = Object.keys(cfCopyFilesPhases).filter( (k) => typeof cfCopyFilesPhases[k] === 'object' && !/_comment$/.test(k), ); let cfAppexBuildFile = null; cfPhaseKeys.forEach((key) => { const phase = cfCopyFilesPhases[key]; const kept = []; (phase.files || []).forEach((f) => { if ( typeof f?.comment === 'string' && f.comment.includes(`${CF_TARGET_NAME}.appex`) ) { cfAppexBuildFile = f; } else { kept.push(f); } }); phase.files = kept; }); if (!cfAppexBuildFile) { throw new Error( '[with-rebreak-protection-ios] ContentFilter-.appex-BuildFile nicht gefunden', ); } const cfDst13Key = cfPhaseKeys.find( (k) => cfCopyFilesPhases[k].dstSubfolderSpec === 13 && (cfCopyFilesPhases[k].files || []).length === 0, ); if (!cfDst13Key) { throw new Error( '[with-rebreak-protection-ios] keine leere dst-13-Copy-Files-Phase für die ContentFilter-.appex gefunden', ); } cfCopyFilesPhases[cfDst13Key].name = '"Embed App Extensions"'; cfCopyFilesPhases[cfDst13Key].files = [cfAppexBuildFile]; const cfDst13CommentKey = `${cfDst13Key}_comment`; if (cfCopyFilesPhases[cfDst13CommentKey] === 'Copy Files') { cfCopyFilesPhases[cfDst13CommentKey] = 'Embed App Extensions'; } const cfNativeTargets = proj.hash.project.objects.PBXNativeTarget || {}; Object.keys(cfNativeTargets).forEach((tk) => { const nt = cfNativeTargets[tk]; if (typeof nt !== 'object' || !Array.isArray(nt.buildPhases)) return; nt.buildPhases.forEach((bp) => { if (bp.value === cfDst13Key && bp.comment === 'Copy Files') { bp.comment = 'Embed App Extensions'; } }); }); const mainTargetUuid = proj.getFirstTarget().uuid; proj.addTargetDependency(mainTargetUuid, [target.uuid]); return cfg; }); } // ─── Composition ──────────────────────────────────────────────────────────── module.exports = function withRebreakProtectionIos(config) { config = withMainAppEntitlements(config); config = withCopyExtensionSources(config); // withExtensionTarget (NEURLFilter, iOS 26 + PIR) entfernt — `url-filter-provider` // ist kein gültiger EAS-Entitlement-Wert. // ContentFilter (klassisches NEFilter) zuerst registrieren — läuft per LIFO // NACH PacketTunnel, was dem Embed-Phase-Fix in beiden Hooks sauber entkoppelt. config = withContentFilterTarget(config); config = withPacketTunnelTarget(config); return config; };