"use strict";
/**
 * DFStreamer IPA Resign Module
 *
 * Resigns a pre-built unsigned DFStreamer IPA with the user's provisioning profile.
 * Mirrors the WDA resign flow but without any xcodebuild step — the IPA is pre-built.
 *
 * The unsigned DFStreamer.ipa must be placed in ~/.cache/device-farm/ (or configured path).
 * Bundle ID is derived from the user's provisioning profile.
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.resignDfStreamer = resignDfStreamer;
const child_process_1 = require("child_process");
const fs_1 = __importDefault(require("fs"));
const http_1 = __importDefault(require("http"));
const https_1 = __importDefault(require("https"));
const os_1 = __importDefault(require("os"));
const path_1 = __importDefault(require("path"));
const util_1 = __importDefault(require("util"));
const form_data_1 = __importDefault(require("form-data"));
const execAsync = util_1.default.promisify(child_process_1.exec);
const node_config_1 = require("../config/node.config");
// ============================================================================
// Helper Functions (reused from prepare-wda.ts patterns)
// ============================================================================
const UNSIGNED_IPA_NAME = 'DFStreamer.ipa';
/**
 * Resolve the path to the bundled unsigned DFStreamer.ipa shipped with the package.
 * Checks both compiled (dist/src/scripts/) and dev (src/scripts/) layouts.
 */
function getBundledDfStreamerPath() {
    // Compiled: dist/src/scripts/ → ../../.. → package root → assets/
    const compiledPath = path_1.default.resolve(__dirname, '../../../assets', UNSIGNED_IPA_NAME);
    if (fs_1.default.existsSync(compiledPath))
        return compiledPath;
    // Dev (ts-node): src/scripts/ → ../.. → package root → assets/
    const devPath = path_1.default.resolve(__dirname, '../../assets', UNSIGNED_IPA_NAME);
    if (fs_1.default.existsSync(devPath))
        return devPath;
    return null;
}
/**
 * Ensure the unsigned DFStreamer.ipa exists in the cache directory.
 * If missing, copies it from the bundled assets shipped with the package.
 */
function ensureUnsignedIpaAvailable(cacheDir) {
    const targetPath = path_1.default.join(cacheDir, UNSIGNED_IPA_NAME);
    if (fs_1.default.existsSync(targetPath))
        return;
    const bundledPath = getBundledDfStreamerPath();
    if (!bundledPath)
        return;
    if (!fs_1.default.existsSync(cacheDir)) {
        fs_1.default.mkdirSync(cacheDir, { recursive: true });
    }
    fs_1.default.copyFileSync(bundledPath, targetPath);
}
/**
 * Get the cache directory where unsigned DFStreamer IPA is expected
 */
function getCacheDir() {
    const config = (0, node_config_1.getMergedConfig)();
    if (config.wdaResignedIpaPath) {
        return config.wdaResignedIpaPath;
    }
    if (process.env.WDA_STORAGE_PATH && process.env.WDA_STORAGE_PATH.trim() !== '') {
        return process.env.WDA_STORAGE_PATH;
    }
    switch (os_1.default.platform()) {
        case 'darwin':
            return path_1.default.join(process.cwd(), '.cache', 'device-farm');
        case 'win32':
            return path_1.default.join(process.env.LOCALAPPDATA || path_1.default.join(os_1.default.homedir(), 'AppData', 'Local'), 'device-farm');
        default:
            return path_1.default.join(os_1.default.homedir(), '.cache', 'device-farm');
    }
}
/**
 * Load required resign dependencies dynamically
 */
function loadDependencies() {
    try {
        // eslint-disable-next-line @typescript-eslint/no-var-requires
        const Applesign = require('applesign');
        // eslint-disable-next-line @typescript-eslint/no-var-requires
        const { provision } = require('ios-mobileprovision-finder');
        return { Applesign, provision };
    }
    catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        throw new Error(`Missing required dependencies for DFStreamer resign: ${errorMessage}. ` +
            `Please install: applesign ios-mobileprovision-finder`);
    }
}
/**
 * Get provisioning profile path based on Xcode version
 */
async function getProvisioningProfilePath() {
    const { exec } = await Promise.resolve().then(() => __importStar(require('child_process')));
    const { promisify } = await Promise.resolve().then(() => __importStar(require('util')));
    const execAsync = promisify(exec);
    try {
        const { stdout } = await execAsync('xcodebuild -version');
        const match = stdout.match(/Xcode (\d+)\./);
        const xcodeVersion = match ? parseInt(match[1], 10) : 16;
        if (xcodeVersion <= 15) {
            return path_1.default.join(os_1.default.homedir(), 'Library/MobileDevice/Provisioning Profiles');
        }
    }
    catch {
        // If xcodebuild not available, use Xcode 16+ default
    }
    return path_1.default.join(os_1.default.homedir(), 'Library/Developer/Xcode/UserData/Provisioning Profiles');
}
/**
 * Get mobile provisioning file path from UUID
 */
async function getMobileProvisioningFilePath(provision, provisioningUUID) {
    const provisionFileDir = await getProvisioningProfilePath();
    if (!fs_1.default.existsSync(provisionFileDir)) {
        throw new Error(`Provisioning directory does not exist: ${provisionFileDir}`);
    }
    const files = fs_1.default
        .readdirSync(provisionFileDir, { encoding: 'utf8' })
        .filter(file => file.endsWith('.mobileprovision'));
    const provisioningFiles = files.map(file => {
        const fullPath = path_1.default.join(provisionFileDir, file);
        const mp = provision.readFromFile(fullPath);
        return { ...mp, _filePath: fullPath };
    });
    if (!provisioningFiles || !provisioningFiles.length) {
        throw new Error('No mobileprovision file found on the machine');
    }
    const selectedProfile = provisioningFiles.find(file => file.UUID === provisioningUUID);
    if (!selectedProfile) {
        throw new Error(`Provisioning profile with UUID ${provisioningUUID} not found`);
    }
    return {
        filePath: selectedProfile._filePath,
        profile: selectedProfile,
    };
}
/**
 * Upload resigned IPA to hub (reuses WDA upload endpoint)
 */
async function uploadToHub(hubUrl, authHeader, ipaPath, bundleId, setAsDefault = false) {
    return new Promise((resolve, reject) => {
        const url = new URL('/api/wda/upload', hubUrl);
        const client = url.protocol === 'https:' ? https_1.default : http_1.default;
        const form = new form_data_1.default();
        form.append('file', fs_1.default.createReadStream(ipaPath));
        form.append('bundleId', bundleId);
        form.append('setAsDefault', setAsDefault.toString());
        // Tag it so hub can distinguish DFStreamer from WDA
        form.append('fileName', path_1.default.basename(ipaPath));
        const headers = {
            ...form.getHeaders(),
            Authorization: authHeader,
        };
        const req = client.request({
            method: 'POST',
            hostname: url.hostname,
            port: url.port,
            path: url.pathname,
            headers,
        }, res => {
            let data = '';
            res.on('data', chunk => (data += chunk));
            res.on('end', () => {
                if (res.statusCode === 200 || res.statusCode === 201) {
                    try {
                        const response = JSON.parse(data);
                        resolve({
                            success: true,
                            message: 'DFStreamer uploaded successfully',
                            wdaId: response.wda?.id,
                        });
                    }
                    catch {
                        resolve({ success: true, message: 'DFStreamer uploaded successfully' });
                    }
                }
                else {
                    resolve({ success: false, message: `Upload failed: HTTP ${res.statusCode}` });
                }
            });
        });
        req.on('error', error => {
            reject(error);
        });
        form.pipe(req);
    });
}
/**
 * Check if hub server is running
 */
async function isHubRunning(hubUrl) {
    return new Promise(resolve => {
        const url = new URL('/health', hubUrl);
        const client = url.protocol === 'https:' ? https_1.default : http_1.default;
        const req = client.get(url.href, { timeout: 3000 }, res => {
            resolve(res.statusCode === 200);
        });
        req.on('error', () => resolve(false));
        req.on('timeout', () => {
            req.destroy();
            resolve(false);
        });
    });
}
// ============================================================================
// IPA Extension Preprocessing
// ============================================================================
/**
 * Get ALL provisioning profile directories (both Xcode 15 and 16+ paths).
 * Profiles can exist in either location depending on Xcode version and history.
 */
function getAllProvisioningProfileDirs() {
    return [
        path_1.default.join(os_1.default.homedir(), 'Library/Developer/Xcode/UserData/Provisioning Profiles'),
        path_1.default.join(os_1.default.homedir(), 'Library/MobileDevice/Provisioning Profiles'),
    ].filter(dir => fs_1.default.existsSync(dir));
}
/**
 * Search installed provisioning profiles for one matching the given bundle ID.
 *
 * Searches BOTH Xcode 15 and 16+ profile directories. Supports:
 * - Exact bundle ID match
 * - Universal wildcard (`TEAMID.*`)
 * - Prefix wildcard (`TEAMID.com.example.*` covers `com.example.app`)
 * - Team ID filtering (only return profiles from the same team)
 * - Expiration checking (skip expired profiles)
 *
 * Returns the file path if found, or null.
 */
async function findProvisioningProfileForBundleId(targetBundleId, teamId, progress) {
    const profileDirs = getAllProvisioningProfileDirs();
    if (profileDirs.length === 0) {
        progress?.('profile-search', 'No provisioning profile directories found');
        return null;
    }
    let bestWildcardMatch = null;
    const now = new Date();
    for (const profileDir of profileDirs) {
        const files = fs_1.default
            .readdirSync(profileDir, { encoding: 'utf8' })
            .filter(file => file.endsWith('.mobileprovision'));
        for (const file of files) {
            const fullPath = path_1.default.join(profileDir, file);
            try {
                const { stdout: profileXml } = await execAsync(`security cms -D -i "${fullPath}" 2>/dev/null`);
                // Check expiration — free account profiles expire in 7 days
                const expiryMatch = profileXml.match(/<key>ExpirationDate<\/key>\s*<date>([^<]+)<\/date>/);
                if (expiryMatch) {
                    const expiry = new Date(expiryMatch[1]);
                    if (expiry < now) {
                        continue; // Skip expired profiles silently
                    }
                }
                // Filter by team ID if provided
                if (teamId) {
                    const profileTeamMatch = profileXml.match(/<key>TeamIdentifier<\/key>\s*<array>\s*<string>([^<]+)<\/string>/);
                    if (profileTeamMatch && profileTeamMatch[1].trim() !== teamId) {
                        continue; // Different team — skip
                    }
                }
                const appIdMatch = profileXml.match(/<key>application-identifier<\/key>\s*<string>([^<]+)<\/string>/);
                if (appIdMatch) {
                    // application-identifier is "TEAMID.bundleId" — strip team prefix
                    const parts = appIdMatch[1].split('.');
                    parts.shift(); // remove team ID
                    const profileBundleId = parts.join('.');
                    // Exact match — best possible result
                    if (profileBundleId === targetBundleId) {
                        progress?.('profile-search', `Found exact profile match: ${file}`);
                        return fullPath;
                    }
                    // Universal wildcard (TEAMID.*)
                    if (profileBundleId === '*') {
                        bestWildcardMatch = bestWildcardMatch || fullPath;
                    }
                    // Prefix wildcard (e.g., com.example.* covers com.example.app.extension)
                    if (profileBundleId.endsWith('.*')) {
                        const prefix = profileBundleId.slice(0, -2);
                        if (targetBundleId.startsWith(prefix + '.') || targetBundleId === prefix) {
                            bestWildcardMatch = fullPath; // Prefix wildcards are more specific, prefer them
                        }
                    }
                }
            }
            catch {
                // Skip unreadable profiles
            }
        }
    }
    if (bestWildcardMatch) {
        progress?.('profile-search', `Found wildcard profile match for ${targetBundleId}: ${path_1.default.basename(bestWildcardMatch)}`);
        return bestWildcardMatch;
    }
    progress?.('profile-search', `No profile found for ${targetBundleId} (searched ${profileDirs.length} directory(ies))`);
    return null;
}
/**
 * Auto-provision a broadcast extension profile using xcodebuild.
 *
 * Creates a minimal Xcode project with a container app + broadcast upload extension,
 * then runs `xcodebuild -resolvePackageDependencies -allowProvisioningUpdates` to
 * trigger automatic provisioning. Xcode will create the App ID and provisioning
 * profile for the extension's bundle ID on Apple's developer portal.
 *
 * Requirements:
 * - Xcode installed with command-line tools
 * - Apple ID configured in Xcode's account preferences
 * - Device registered with the free account (done automatically by Xcode)
 *
 * Returns the path to the generated profile, or null if auto-provisioning failed.
 */
async function autoProvisionExtensionProfile(teamId, containerBundleId, extBundleId, progress) {
    const tempDir = path_1.default.join(os_1.default.tmpdir(), `df-auto-provision-${Date.now()}`);
    try {
        fs_1.default.mkdirSync(tempDir, { recursive: true });
        // Create minimal source files
        const appSrcDir = path_1.default.join(tempDir, 'TempApp');
        const extSrcDir = path_1.default.join(tempDir, 'TempExtension');
        fs_1.default.mkdirSync(appSrcDir);
        fs_1.default.mkdirSync(extSrcDir);
        fs_1.default.writeFileSync(path_1.default.join(appSrcDir, 'AppDelegate.swift'), `import UIKit\n@UIApplicationMain\nclass AppDelegate: UIResponder, UIApplicationDelegate {\n  var window: UIWindow?\n}\n`);
        fs_1.default.writeFileSync(path_1.default.join(extSrcDir, 'SampleHandler.swift'), `import ReplayKit\nclass SampleHandler: RPBroadcastSampleHandler {}\n`);
        // Create Info.plist files
        const appInfoPlist = [
            '<?xml version="1.0" encoding="UTF-8"?>',
            '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
            '<plist version="1.0"><dict>',
            '<key>CFBundleExecutable</key><string>$(EXECUTABLE_NAME)</string>',
            '<key>CFBundleIdentifier</key><string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>',
            '<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>',
            '<key>CFBundleName</key><string>TempApp</string>',
            '<key>CFBundlePackageType</key><string>APPL</string>',
            '<key>CFBundleShortVersionString</key><string>1.0</string>',
            '<key>CFBundleVersion</key><string>1</string>',
            '<key>UILaunchStoryboardName</key><string></string>',
            '</dict></plist>',
        ].join('\n');
        fs_1.default.writeFileSync(path_1.default.join(appSrcDir, 'Info.plist'), appInfoPlist);
        const extInfoPlist = [
            '<?xml version="1.0" encoding="UTF-8"?>',
            '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
            '<plist version="1.0"><dict>',
            '<key>CFBundleExecutable</key><string>$(EXECUTABLE_NAME)</string>',
            '<key>CFBundleIdentifier</key><string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>',
            '<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>',
            '<key>CFBundleName</key><string>TempExtension</string>',
            '<key>CFBundlePackageType</key><string>XPC!</string>',
            '<key>CFBundleShortVersionString</key><string>1.0</string>',
            '<key>CFBundleVersion</key><string>1</string>',
            '<key>NSExtension</key><dict>',
            '<key>NSExtensionPointIdentifier</key><string>com.apple.broadcast-services-upload</string>',
            '<key>NSExtensionPrincipalClass</key><string>$(PRODUCT_MODULE_NAME).SampleHandler</string>',
            '<key>RPBroadcastProcessMode</key><string>RPBroadcastProcessModeSampleBuffer</string>',
            '</dict>',
            '</dict></plist>',
        ].join('\n');
        fs_1.default.writeFileSync(path_1.default.join(extSrcDir, 'Info.plist'), extInfoPlist);
        // Generate project.pbxproj with deterministic UUIDs
        const projDir = path_1.default.join(tempDir, 'TempApp.xcodeproj');
        fs_1.default.mkdirSync(projDir);
        const pbxproj = generateMinimalPbxproj(containerBundleId, extBundleId, teamId);
        fs_1.default.writeFileSync(path_1.default.join(projDir, 'project.pbxproj'), pbxproj);
        progress('auto-provision', `Auto-provisioning extension profile for ${extBundleId}...`);
        // Run xcodebuild to trigger automatic provisioning.
        // We use -resolvePackageDependencies first (lightweight) and then a build attempt.
        // Even if the build fails (no real app logic), the profile is often created.
        try {
            await execAsync(`xcodebuild -project "${projDir}" ` +
                `-target TempExtension ` +
                `-allowProvisioningUpdates ` +
                `-allowProvisioningDeviceRegistration ` +
                `DEVELOPMENT_TEAM="${teamId}" ` +
                `CODE_SIGN_IDENTITY="Apple Development" ` +
                `CODE_SIGN_STYLE=Automatic ` +
                `PRODUCT_BUNDLE_IDENTIFIER="${extBundleId}" ` +
                `-destination "generic/platform=iOS" ` +
                `-quiet ` +
                `build 2>&1`, { timeout: 120000 });
            progress('auto-provision', 'xcodebuild succeeded — searching for generated profile');
        }
        catch (buildErr) {
            // Build might fail (missing frameworks, etc.) but the profile is often created anyway
            // during the provisioning resolution phase
            progress('auto-provision', 'xcodebuild build failed (expected for temp project) — checking if profile was created');
        }
        // Search for the newly created profile
        const newProfile = await findProvisioningProfileForBundleId(extBundleId, teamId, progress);
        if (newProfile) {
            progress('auto-provision', `Auto-provisioned profile found: ${path_1.default.basename(newProfile)}`);
            return newProfile;
        }
        progress('auto-provision', 'Auto-provisioning did not produce a matching profile');
        return null;
    }
    catch (err) {
        progress('auto-provision', `Auto-provisioning failed: ${err instanceof Error ? err.message : String(err)}`);
        return null;
    }
    finally {
        // Clean up temp project
        if (fs_1.default.existsSync(tempDir)) {
            fs_1.default.rmSync(tempDir, { recursive: true });
        }
    }
}
/**
 * Generate a minimal project.pbxproj for auto-provisioning.
 * Contains a container app target and a broadcast upload extension target.
 * Uses automatic signing with the specified team ID.
 */
function generateMinimalPbxproj(containerBundleId, extBundleId, teamId) {
    // Fixed UUIDs for the temp project (deterministic, doesn't matter — project is disposable)
    const IDs = {
        project: 'AA0000000000000000000001',
        mainGroup: 'AA0000000000000000000002',
        appGroup: 'AA0000000000000000000003',
        extGroup: 'AA0000000000000000000004',
        productsGroup: 'AA0000000000000000000005',
        appTarget: 'AA0000000000000000000010',
        extTarget: 'AA0000000000000000000011',
        appProduct: 'AA0000000000000000000020',
        extProduct: 'AA0000000000000000000021',
        appDelegate: 'AA0000000000000000000030',
        sampleHandler: 'AA0000000000000000000031',
        appInfoPlist: 'AA0000000000000000000032',
        extInfoPlist: 'AA0000000000000000000033',
        appDelegateBF: 'AA0000000000000000000040',
        sampleHandlerBF: 'AA0000000000000000000041',
        appSources: 'AA0000000000000000000050',
        extSources: 'AA0000000000000000000051',
        appFrameworks: 'AA0000000000000000000052',
        extFrameworks: 'AA0000000000000000000053',
        appDebugCfg: 'AA0000000000000000000060',
        appReleaseCfg: 'AA0000000000000000000061',
        extDebugCfg: 'AA0000000000000000000062',
        extReleaseCfg: 'AA0000000000000000000063',
        projDebugCfg: 'AA0000000000000000000064',
        projReleaseCfg: 'AA0000000000000000000065',
        appCfgList: 'AA0000000000000000000070',
        extCfgList: 'AA0000000000000000000071',
        projCfgList: 'AA0000000000000000000072',
        appDepProxy: 'AA0000000000000000000080',
        appDepTarget: 'AA0000000000000000000081',
        appEmbedExt: 'AA0000000000000000000082',
        extProductBF: 'AA0000000000000000000083',
    };
    return `// !$*UTF8*$!
{
  archiveVersion = 1;
  classes = {};
  objectVersion = 56;
  objects = {
    ${IDs.appDelegateBF} = {isa = PBXBuildFile; fileRef = ${IDs.appDelegate};};
    ${IDs.sampleHandlerBF} = {isa = PBXBuildFile; fileRef = ${IDs.sampleHandler};};
    ${IDs.extProductBF} = {isa = PBXBuildFile; fileRef = ${IDs.extProduct}; settings = {ATTRIBUTES = (RemoveHeadersOnCopy,);};};
    ${IDs.appDelegate} = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>";};
    ${IDs.sampleHandler} = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleHandler.swift; sourceTree = "<group>";};
    ${IDs.appInfoPlist} = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>";};
    ${IDs.extInfoPlist} = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>";};
    ${IDs.appProduct} = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TempApp.app; sourceTree = BUILT_PRODUCTS_DIR;};
    ${IDs.extProduct} = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = TempExtension.appex; sourceTree = BUILT_PRODUCTS_DIR;};
    ${IDs.mainGroup} = {isa = PBXGroup; children = (${IDs.appGroup}, ${IDs.extGroup}, ${IDs.productsGroup}); sourceTree = "<group>";};
    ${IDs.appGroup} = {isa = PBXGroup; children = (${IDs.appDelegate}, ${IDs.appInfoPlist}); path = TempApp; sourceTree = "<group>";};
    ${IDs.extGroup} = {isa = PBXGroup; children = (${IDs.sampleHandler}, ${IDs.extInfoPlist}); path = TempExtension; sourceTree = "<group>";};
    ${IDs.productsGroup} = {isa = PBXGroup; children = (${IDs.appProduct}, ${IDs.extProduct}); name = Products; sourceTree = "<group>";};
    ${IDs.appSources} = {isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = (${IDs.appDelegateBF}); runOnlyForDeploymentPostprocessing = 0;};
    ${IDs.extSources} = {isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = (${IDs.sampleHandlerBF}); runOnlyForDeploymentPostprocessing = 0;};
    ${IDs.appFrameworks} = {isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = (); runOnlyForDeploymentPostprocessing = 0;};
    ${IDs.extFrameworks} = {isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = (); runOnlyForDeploymentPostprocessing = 0;};
    ${IDs.appEmbedExt} = {isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 13; files = (${IDs.extProductBF}); name = "Embed App Extensions"; runOnlyForDeploymentPostprocessing = 0;};
    ${IDs.appDepProxy} = {isa = PBXContainerItemProxy; containerPortal = ${IDs.project}; proxyType = 1; remoteGlobalIDString = ${IDs.extTarget}; remoteInfo = TempExtension;};
    ${IDs.appDepTarget} = {isa = PBXTargetDependency; target = ${IDs.extTarget}; targetProxy = ${IDs.appDepProxy};};
    ${IDs.appTarget} = {
      isa = PBXNativeTarget;
      buildConfigurationList = ${IDs.appCfgList};
      buildPhases = (${IDs.appSources}, ${IDs.appFrameworks}, ${IDs.appEmbedExt});
      buildRules = ();
      dependencies = (${IDs.appDepTarget});
      name = TempApp;
      productName = TempApp;
      productReference = ${IDs.appProduct};
      productType = "com.apple.product-type.application";
    };
    ${IDs.extTarget} = {
      isa = PBXNativeTarget;
      buildConfigurationList = ${IDs.extCfgList};
      buildPhases = (${IDs.extSources}, ${IDs.extFrameworks});
      buildRules = ();
      dependencies = ();
      name = TempExtension;
      productName = TempExtension;
      productReference = ${IDs.extProduct};
      productType = "com.apple.product-type.app-extension";
    };
    ${IDs.project} = {
      isa = PBXProject;
      attributes = {LastSwiftUpdateCheck = 1500; LastUpgradeCheck = 1500;
        TargetAttributes = {
          ${IDs.appTarget} = {DevelopmentTeam = ${teamId}; ProvisioningStyle = Automatic;};
          ${IDs.extTarget} = {DevelopmentTeam = ${teamId}; ProvisioningStyle = Automatic;};
        };
      };
      buildConfigurationList = ${IDs.projCfgList};
      compatibilityVersion = "Xcode 14.0";
      developmentRegion = en;
      hasScannedForEncodings = 0;
      knownRegions = (en);
      mainGroup = ${IDs.mainGroup};
      productRefGroup = ${IDs.productsGroup};
      projectDirPath = "";
      projectRoot = "";
      targets = (${IDs.appTarget}, ${IDs.extTarget});
    };
    ${IDs.appDebugCfg} = {
      isa = XCBuildConfiguration; buildSettings = {
        ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
        CODE_SIGN_STYLE = Automatic;
        DEVELOPMENT_TEAM = ${teamId};
        INFOPLIST_FILE = TempApp/Info.plist;
        IPHONEOS_DEPLOYMENT_TARGET = 15.0;
        PRODUCT_BUNDLE_IDENTIFIER = "${containerBundleId}";
        PRODUCT_NAME = "$(TARGET_NAME)";
        SWIFT_VERSION = 5.0;
        TARGETED_DEVICE_FAMILY = "1,2";
      }; name = Debug;
    };
    ${IDs.appReleaseCfg} = {
      isa = XCBuildConfiguration; buildSettings = {
        ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
        CODE_SIGN_STYLE = Automatic;
        DEVELOPMENT_TEAM = ${teamId};
        INFOPLIST_FILE = TempApp/Info.plist;
        IPHONEOS_DEPLOYMENT_TARGET = 15.0;
        PRODUCT_BUNDLE_IDENTIFIER = "${containerBundleId}";
        PRODUCT_NAME = "$(TARGET_NAME)";
        SWIFT_VERSION = 5.0;
        TARGETED_DEVICE_FAMILY = "1,2";
      }; name = Release;
    };
    ${IDs.extDebugCfg} = {
      isa = XCBuildConfiguration; buildSettings = {
        CODE_SIGN_STYLE = Automatic;
        DEVELOPMENT_TEAM = ${teamId};
        INFOPLIST_FILE = TempExtension/Info.plist;
        IPHONEOS_DEPLOYMENT_TARGET = 15.0;
        PRODUCT_BUNDLE_IDENTIFIER = "${extBundleId}";
        PRODUCT_NAME = "$(TARGET_NAME)";
        SWIFT_VERSION = 5.0;
        TARGETED_DEVICE_FAMILY = "1,2";
      }; name = Debug;
    };
    ${IDs.extReleaseCfg} = {
      isa = XCBuildConfiguration; buildSettings = {
        CODE_SIGN_STYLE = Automatic;
        DEVELOPMENT_TEAM = ${teamId};
        INFOPLIST_FILE = TempExtension/Info.plist;
        IPHONEOS_DEPLOYMENT_TARGET = 15.0;
        PRODUCT_BUNDLE_IDENTIFIER = "${extBundleId}";
        PRODUCT_NAME = "$(TARGET_NAME)";
        SWIFT_VERSION = 5.0;
        TARGETED_DEVICE_FAMILY = "1,2";
      }; name = Release;
    };
    ${IDs.projDebugCfg} = {
      isa = XCBuildConfiguration; buildSettings = {
        ALWAYS_SEARCH_USER_PATHS = NO;
        CLANG_ENABLE_MODULES = YES;
        COPY_PHASE_STRIP = NO;
        IPHONEOS_DEPLOYMENT_TARGET = 15.0;
        SDKROOT = iphoneos;
        SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
        SWIFT_OPTIMIZATION_LEVEL = "-Onone";
      }; name = Debug;
    };
    ${IDs.projReleaseCfg} = {
      isa = XCBuildConfiguration; buildSettings = {
        ALWAYS_SEARCH_USER_PATHS = NO;
        CLANG_ENABLE_MODULES = YES;
        COPY_PHASE_STRIP = YES;
        IPHONEOS_DEPLOYMENT_TARGET = 15.0;
        SDKROOT = iphoneos;
        SWIFT_OPTIMIZATION_LEVEL = "-O";
      }; name = Release;
    };
    ${IDs.appCfgList} = {isa = XCConfigurationList; buildConfigurations = (${IDs.appDebugCfg}, ${IDs.appReleaseCfg}); defaultConfigurationIsVisible = 0; defaultConfigurationName = Debug;};
    ${IDs.extCfgList} = {isa = XCConfigurationList; buildConfigurations = (${IDs.extDebugCfg}, ${IDs.extReleaseCfg}); defaultConfigurationIsVisible = 0; defaultConfigurationName = Debug;};
    ${IDs.projCfgList} = {isa = XCConfigurationList; buildConfigurations = (${IDs.projDebugCfg}, ${IDs.projReleaseCfg}); defaultConfigurationIsVisible = 0; defaultConfigurationName = Debug;};
  };
  rootObject = ${IDs.project};
}
`;
}
/**
 * Preprocess an IPA to fix extension bundle IDs and provisioning profiles.
 *
 * applesign does NOT update PlugIns/*.appex Info.plist bundle IDs or copy
 * embedded.mobileprovision into extensions. This causes installation failures
 * for apps with broadcast extensions (ReplayKit) or other app extensions.
 *
 * This function:
 * 1. Unzips the IPA
 * 2. Updates each extension's CFBundleIdentifier to <parentBundleId>.<extensionSuffix>
 * 3. Copies the provisioning profile into each extension
 * 4. Re-zips into a modified archive for applesign to consume
 *
 * Returns the IPA path and a mapping of extension names to their correct bundle IDs.
 * The mapping is needed because applesign may overwrite extension bundle IDs.
 */
async function preprocessIpaExtensions(ipaPath, workDir, bundleId, mobileProvisioningFile, extensionProvisioningFile, isFreeAccount, progress) {
    const tempDir = path_1.default.join(workDir, 'temp-resign');
    if (fs_1.default.existsSync(tempDir)) {
        fs_1.default.rmSync(tempDir, { recursive: true });
    }
    fs_1.default.mkdirSync(tempDir, { recursive: true });
    try {
        await execAsync(`unzip -o -q "${ipaPath}" -d "${tempDir}"`);
        const payloadDir = path_1.default.join(tempDir, 'Payload');
        if (!fs_1.default.existsSync(payloadDir)) {
            fs_1.default.rmSync(tempDir, { recursive: true });
            return { ipaPath, extensionBundleIds: {} };
        }
        const appDirs = fs_1.default.readdirSync(payloadDir).filter(f => f.endsWith('.app'));
        if (appDirs.length === 0) {
            fs_1.default.rmSync(tempDir, { recursive: true });
            return { ipaPath, extensionBundleIds: {} };
        }
        const appDir = path_1.default.join(payloadDir, appDirs[0]);
        // Update main app bundle ID (do this here so applesign doesn't need bundleId option,
        // which would overwrite our extension bundle ID fixes)
        const trimmedBundleId = bundleId.replace(/^\s+|\s+$/g, '');
        const mainInfoPlist = path_1.default.join(appDir, 'Info.plist');
        if (fs_1.default.existsSync(mainInfoPlist)) {
            await execAsync(`/usr/bin/plutil -replace CFBundleIdentifier -string "${trimmedBundleId}" "${mainInfoPlist}"`);
            progress('preprocess', `Updated main app bundle ID → ${trimmedBundleId}`);
        }
        // Copy provisioning profile into main app
        const mainMobileProvision = path_1.default.join(appDir, 'embedded.mobileprovision');
        fs_1.default.copyFileSync(mobileProvisioningFile, mainMobileProvision);
        const pluginsDir = path_1.default.join(appDir, 'PlugIns');
        if (!fs_1.default.existsSync(pluginsDir)) {
            // No extensions, but we still modified main app — re-zip below
            const modifiedZipPath = path_1.default.join(workDir, 'dfstreamer-modified.zip');
            if (fs_1.default.existsSync(modifiedZipPath)) {
                fs_1.default.unlinkSync(modifiedZipPath);
            }
            await execAsync(`cd "${tempDir}" && /usr/bin/zip -r -q "${modifiedZipPath}" Payload/`);
            fs_1.default.rmSync(tempDir, { recursive: true });
            progress('preprocess', 'Preprocessed IPA (main app bundle ID updated, no extensions)');
            return { ipaPath: modifiedZipPath, extensionBundleIds: {} };
        }
        const extensions = fs_1.default.readdirSync(pluginsDir).filter(f => f.endsWith('.appex'));
        if (extensions.length === 0) {
            const modifiedZipPath = path_1.default.join(workDir, 'dfstreamer-modified.zip');
            if (fs_1.default.existsSync(modifiedZipPath)) {
                fs_1.default.unlinkSync(modifiedZipPath);
            }
            await execAsync(`cd "${tempDir}" && /usr/bin/zip -r -q "${modifiedZipPath}" Payload/`);
            fs_1.default.rmSync(tempDir, { recursive: true });
            progress('preprocess', 'Preprocessed IPA (main app bundle ID updated, no extensions)');
            return { ipaPath: modifiedZipPath, extensionBundleIds: {} };
        }
        // Extract team ID from main profile once (same for all extensions)
        const mainProfileEntPreprocess = await extractProfileEntitlements(mobileProvisioningFile);
        const mainTeamId = mainProfileEntPreprocess?.teamId;
        const mainAppIdIsWildcard = mainProfileEntPreprocess
            ? mainProfileEntPreprocess.appId.endsWith('.*') ||
                mainProfileEntPreprocess.appId.endsWith('*')
            : false;
        // Track correct extension bundle IDs — applesign may overwrite these,
        // so fixExtensionEntitlements needs them to restore the correct values.
        const extensionBundleIds = {};
        for (const ext of extensions) {
            const extDir = path_1.default.join(pluginsDir, ext);
            const extInfoPlist = path_1.default.join(extDir, 'Info.plist');
            if (fs_1.default.existsSync(extInfoPlist)) {
                // Read original extension bundle ID (plutil handles both binary and XML plist)
                const { stdout: originalExtBundleId } = await execAsync(`/usr/bin/plutil -extract CFBundleIdentifier raw "${extInfoPlist}"`);
                // Derive extension suffix from original bundle ID (last component)
                const parts = originalExtBundleId.trim().split('.');
                const extSuffix = parts[parts.length - 1];
                const newExtBundleId = `${trimmedBundleId}.${extSuffix}`;
                await execAsync(`/usr/bin/plutil -replace CFBundleIdentifier -string "${newExtBundleId}" "${extInfoPlist}"`);
                // Store the correct bundle ID for this extension
                extensionBundleIds[ext] = newExtBundleId;
                progress('preprocess', `Updated extension ${ext} bundle ID: ${originalExtBundleId.trim()} → ${newExtBundleId}`);
            }
            // Find and copy the correct provisioning profile for this extension.
            // The extension needs a profile matching its own bundle ID — the main app's
            // explicit profile won't work (iOS validates application-identifier match).
            const newExtBundleIdForProfile = fs_1.default.existsSync(extInfoPlist)
                ? (await execAsync(`/usr/bin/plutil -extract CFBundleIdentifier raw "${extInfoPlist}"`)).stdout.trim()
                : null;
            let extProfilePath = mobileProvisioningFile; // fallback: use main app's profile
            if (extensionProvisioningFile) {
                // Explicitly provided extension profile takes priority
                extProfilePath = extensionProvisioningFile;
                progress('preprocess', `Using provided extension profile for ${ext}`);
            }
            else if (newExtBundleIdForProfile) {
                const matchingProfile = await findProvisioningProfileForBundleId(newExtBundleIdForProfile, mainTeamId, progress);
                if (matchingProfile) {
                    extProfilePath = matchingProfile;
                    progress('preprocess', `Auto-detected profile for extension ${ext}: ${path_1.default.basename(matchingProfile)}`);
                }
                else if (!mainAppIdIsWildcard) {
                    // Main profile is explicit — it CANNOT cover the extension's bundle ID.
                    // Try auto-provisioning: create a temp Xcode project to generate the extension profile.
                    if (mainTeamId) {
                        progress('preprocess', `No extension profile found — attempting auto-provisioning for ${newExtBundleIdForProfile}...`);
                        const autoProfile = await autoProvisionExtensionProfile(mainTeamId, trimmedBundleId, newExtBundleIdForProfile, progress);
                        if (autoProfile) {
                            extProfilePath = autoProfile;
                            progress('preprocess', `Auto-provisioned extension profile: ${path_1.default.basename(autoProfile)}`);
                        }
                        else if (isFreeAccount) {
                            throw new Error(`No provisioning profile found for broadcast extension "${newExtBundleIdForProfile}". ` +
                                `Auto-provisioning failed. Free Apple Developer accounts use explicit App IDs — ` +
                                `the main app profile (${mainProfileEntPreprocess?.appId || 'unknown'}) cannot cover the extension. ` +
                                `\n\nTo fix this:\n` +
                                `1. Open Xcode and create a new iOS project\n` +
                                `2. Add a "Broadcast Upload Extension" target with bundle ID "${newExtBundleIdForProfile}"\n` +
                                `3. Select your free Apple account for signing (Team: ${mainTeamId})\n` +
                                `4. Build the project once (Xcode will auto-create the provisioning profile)\n` +
                                `5. Retry the resign — the extension profile will be found automatically\n` +
                                `\nAlternatively, provide the extension profile UUID via extensionProvisioningUUID.`);
                        }
                        else {
                            progress('preprocess', `⚠ No profile found for ${newExtBundleIdForProfile} and auto-provisioning failed. ` +
                                `Extension may fail to install. ` +
                                `Consider using a wildcard provisioning profile or provide extensionProvisioningUUID.`);
                        }
                    }
                    else if (isFreeAccount) {
                        throw new Error(`No provisioning profile found for broadcast extension "${newExtBundleIdForProfile}". ` +
                            `Could not determine team ID for auto-provisioning. ` +
                            `Please provide the extension profile UUID via extensionProvisioningUUID.`);
                    }
                    else {
                        progress('preprocess', `⚠ No profile found for ${newExtBundleIdForProfile} and main profile is explicit ` +
                            `(${mainProfileEntPreprocess?.appId}). Extension may fail to install. ` +
                            `Consider using a wildcard provisioning profile or provide extensionProvisioningUUID.`);
                    }
                }
                else {
                    // Wildcard main profile — it covers the extension's bundle ID
                    progress('preprocess', `Using wildcard main profile for extension ${ext} (covers ${newExtBundleIdForProfile})`);
                }
            }
            const extMobileProvision = path_1.default.join(extDir, 'embedded.mobileprovision');
            fs_1.default.copyFileSync(extProfilePath, extMobileProvision);
        }
        // Re-zip the modified Payload into a new archive
        const modifiedZipPath = path_1.default.join(workDir, 'dfstreamer-modified.zip');
        if (fs_1.default.existsSync(modifiedZipPath)) {
            fs_1.default.unlinkSync(modifiedZipPath);
        }
        await execAsync(`cd "${tempDir}" && /usr/bin/zip -r -q "${modifiedZipPath}" Payload/`);
        fs_1.default.rmSync(tempDir, { recursive: true });
        progress('preprocess', `Preprocessed IPA with ${extensions.length} extension(s) fixed`);
        return { ipaPath: modifiedZipPath, extensionBundleIds };
    }
    catch (error) {
        if (fs_1.default.existsSync(tempDir)) {
            fs_1.default.rmSync(tempDir, { recursive: true });
        }
        throw error;
    }
}
// ============================================================================
// Post-sign Extension Entitlement Fix
// ============================================================================
/**
 * Extract entitlements from an embedded provisioning profile.
 * Returns the Entitlements dict as XML string, or null if extraction fails.
 */
async function extractProfileEntitlements(profilePath) {
    try {
        const { stdout: profileXml } = await execAsync(`security cms -D -i "${profilePath}" 2>/dev/null`);
        const teamMatch = profileXml.match(/<key>TeamIdentifier<\/key>\s*<array>\s*<string>([^<]+)<\/string>/);
        if (!teamMatch)
            return null;
        const appIdMatch = profileXml.match(/<key>application-identifier<\/key>\s*<string>([^<]+)<\/string>/);
        if (!appIdMatch)
            return null;
        // Extract the full Entitlements dict
        const entStart = profileXml.indexOf('<key>Entitlements</key>');
        if (entStart === -1)
            return null;
        const dictStart = profileXml.indexOf('<dict>', entStart);
        const dictEnd = profileXml.indexOf('</dict>', dictStart);
        if (dictStart === -1 || dictEnd === -1)
            return null;
        const entXml = profileXml.substring(dictStart, dictEnd + '</dict>'.length);
        const hasAppGroups = entXml.includes('application-groups');
        return {
            teamId: teamMatch[1].trim(),
            appId: appIdMatch[1].trim(),
            entXml,
            hasAppGroups,
        };
    }
    catch {
        return null;
    }
}
/**
 * Build an entitlements plist from a provisioning profile's entitlements,
 * overriding application-identifier with the correct value for a specific bundle ID.
 *
 * This approach only includes entitlements the profile actually authorizes,
 * so it works with any profile type (wildcard, explicit, with/without App Groups).
 */
function buildEntitlementsPlist(teamId, targetBundleId, profileEntXml) {
    // Start with the profile's entitlements and override application-identifier
    let ent = profileEntXml;
    // Replace application-identifier value with the correct one for this target
    ent = ent.replace(/(<key>application-identifier<\/key>\s*<string>)[^<]+(<\/string>)/, `$1${teamId}.${targetBundleId}$2`);
    return [
        '<?xml version="1.0" encoding="UTF-8"?>',
        '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
        '<plist version="1.0">',
        ent,
        '</plist>',
    ].join('\n');
}
/**
 * Fix extension bundle IDs and entitlements after applesign has signed the IPA.
 *
 * applesign has THREE problems with broadcast extensions:
 * 1. Overwrites extension CFBundleIdentifier to the main app's value (sub-bundle collision)
 * 2. Strips `com.apple.security.application-groups` (needed for ReplayKit IPC on paid accounts)
 * 3. Sets extension's `application-identifier` entitlement to the main app's value
 *
 * Problem #1 causes: "The parent bundle has the same identifier as sub-bundle" install error.
 * Problem #3 causes: iOS refuses to register the extension (broadcast picker ignores it).
 *
 * This function:
 * 1. Restores each extension's correct CFBundleIdentifier from the pre-applesign mapping
 * 2. Re-signs inside-out (frameworks → extension → main app) with correct per-target entitlements
 *
 * The inside-out order is critical: codesign validates nested code signatures,
 * so frameworks must be signed before their parent bundle.
 *
 * Works with wildcard profiles (no App Groups) and explicit profiles (with App Groups).
 * Throws on failure — a broken extension signature must not be shipped.
 */
async function fixExtensionEntitlements(signedIpaPath, bundleId, _mobileProvisioningFile, extensionBundleIds, progress) {
    const tempDir = path_1.default.join(path_1.default.dirname(signedIpaPath), 'temp-fix-ext');
    if (fs_1.default.existsSync(tempDir)) {
        fs_1.default.rmSync(tempDir, { recursive: true });
    }
    fs_1.default.mkdirSync(tempDir, { recursive: true });
    try {
        await execAsync(`unzip -o -q "${signedIpaPath}" -d "${tempDir}"`);
        const payloadDir = path_1.default.join(tempDir, 'Payload');
        if (!fs_1.default.existsSync(payloadDir))
            return;
        const appDirs = fs_1.default.readdirSync(payloadDir).filter(f => f.endsWith('.app'));
        if (appDirs.length === 0)
            return;
        const appDir = path_1.default.join(payloadDir, appDirs[0]);
        const pluginsDir = path_1.default.join(appDir, 'PlugIns');
        if (!fs_1.default.existsSync(pluginsDir))
            return;
        const extensions = fs_1.default.readdirSync(pluginsDir).filter(f => f.endsWith('.appex'));
        if (extensions.length === 0)
            return;
        // Extract signing identity from the signed main app
        const { stdout: codesignOutput } = await execAsync(`codesign -dvv "${appDir}" 2>&1 || true`);
        const identityMatch = codesignOutput.match(/Authority=(.*)/);
        if (!identityMatch) {
            throw new Error('Could not extract signing identity from signed app. ' +
                'Ensure the provisioning profile has a valid signing certificate.');
        }
        const signingIdentity = identityMatch[1].trim();
        const trimmedBundleId = bundleId.replace(/^\s+|\s+$/g, '');
        // Read main app's embedded profile entitlements
        const mainProfilePath = path_1.default.join(appDir, 'embedded.mobileprovision');
        const mainProfileEnt = await extractProfileEntitlements(mainProfilePath);
        if (!mainProfileEnt) {
            throw new Error('Could not extract entitlements from main app provisioning profile. ' +
                'The profile may be corrupt or missing.');
        }
        // Fix each extension — sign inside-out: frameworks → extension → main app
        // This matches Xcode's signing order and is required because codesign validates
        // nested code signatures when signing a parent bundle.
        for (const ext of extensions) {
            const extDir = path_1.default.join(pluginsDir, ext);
            const extInfoPlist = path_1.default.join(extDir, 'Info.plist');
            if (!fs_1.default.existsSync(extInfoPlist))
                continue;
            const { stdout: extBundleIdRaw } = await execAsync(`/usr/bin/plutil -extract CFBundleIdentifier raw "${extInfoPlist}"`);
            let extBundleId = extBundleIdRaw.trim();
            // Restore extension bundle ID if applesign overwrote it to match the main app's.
            // applesign sets ALL targets' CFBundleIdentifier from the provisioning profile,
            // causing "parent bundle has same identifier as sub-bundle" install errors.
            const correctExtBundleId = extensionBundleIds[ext];
            if (correctExtBundleId && extBundleId !== correctExtBundleId) {
                await execAsync(`/usr/bin/plutil -replace CFBundleIdentifier -string "${correctExtBundleId}" "${extInfoPlist}"`);
                progress('fix-ext', `Restored extension ${ext} bundle ID: ${extBundleId} → ${correctExtBundleId}`);
                extBundleId = correctExtBundleId;
            }
            // Read the extension's own embedded profile (may differ from main app's)
            const extProfilePath = path_1.default.join(extDir, 'embedded.mobileprovision');
            const extProfileEnt = fs_1.default.existsSync(extProfilePath)
                ? await extractProfileEntitlements(extProfilePath)
                : null;
            // Use extension's own profile if available, otherwise fall back to main app's
            const profileEnt = extProfileEnt || mainProfileEnt;
            // Build entitlements from what the profile actually authorizes
            const entPlist = buildEntitlementsPlist(profileEnt.teamId, extBundleId, profileEnt.entXml);
            const entFile = path_1.default.join(tempDir, `${ext}.entitlements.plist`);
            fs_1.default.writeFileSync(entFile, entPlist, 'utf8');
            // Step 1: Re-sign all frameworks inside the extension FIRST (inside-out).
            // applesign signs these initially, but after unzip the signatures may need
            // refreshing. codesign on the parent .appex will fail if nested code is invalid.
            await signNestedFrameworks(extDir, signingIdentity, progress);
            // Step 2: Re-sign the extension with correct entitlements
            await execAsync(`codesign --force --sign "${signingIdentity}" --entitlements "${entFile}" "${extDir}"`);
            progress('fix-ext', `Fixed extension ${ext}: ${extBundleId} (profile: ${extProfileEnt ? 'extension-specific' : 'main-app'}, app-groups: ${profileEnt.hasAppGroups ? 'yes' : 'no'})`);
        }
        // Re-sign main app (extension re-signing invalidated app signature).
        // Use entitlements from the main app's own profile.
        const mainEntPlist = buildEntitlementsPlist(mainProfileEnt.teamId, trimmedBundleId, mainProfileEnt.entXml);
        const mainEntFile = path_1.default.join(tempDir, 'main.entitlements.plist');
        fs_1.default.writeFileSync(mainEntFile, mainEntPlist, 'utf8');
        // Re-sign frameworks inside the main app before re-signing the app itself
        await signNestedFrameworks(appDir, signingIdentity, progress);
        await execAsync(`codesign --force --sign "${signingIdentity}" --entitlements "${mainEntFile}" "${appDir}"`);
        // Verify the entire bundle signature is valid (deep check covers extensions + frameworks)
        try {
            await execAsync(`codesign --verify --deep --strict "${appDir}" 2>&1`);
            progress('fix-ext', 'Code signature verification passed');
        }
        catch (verifyErr) {
            const verifyMsg = verifyErr instanceof Error ? verifyErr.message : String(verifyErr);
            throw new Error(`Code signature verification failed after re-signing: ${verifyMsg}`);
        }
        // Re-zip over the original signed IPA
        fs_1.default.unlinkSync(signedIpaPath);
        await execAsync(`cd "${tempDir}" && /usr/bin/zip -r -q "${signedIpaPath}" Payload/`);
        progress('fix-ext', `Extension entitlements fixed (team: ${mainProfileEnt.teamId}, app-groups: ${mainProfileEnt.hasAppGroups ? 'yes' : 'no'})`);
    }
    finally {
        if (fs_1.default.existsSync(tempDir)) {
            fs_1.default.rmSync(tempDir, { recursive: true });
        }
    }
}
/**
 * Re-sign all frameworks and dylibs inside a bundle's Frameworks/ directory.
 * Must be called BEFORE signing the parent bundle (inside-out signing order).
 *
 * codesign on a parent bundle validates nested code signatures. If frameworks
 * inside the bundle have stale or invalid signatures (e.g. after zip/unzip cycles),
 * the parent signing will fail. Re-signing them first ensures consistency.
 */
async function signNestedFrameworks(bundlePath, signingIdentity, progress) {
    const frameworksDir = path_1.default.join(bundlePath, 'Frameworks');
    if (!fs_1.default.existsSync(frameworksDir))
        return;
    const items = fs_1.default.readdirSync(frameworksDir);
    for (const item of items) {
        const itemPath = path_1.default.join(frameworksDir, item);
        if (item.endsWith('.framework') || item.endsWith('.dylib')) {
            await execAsync(`codesign --force --sign "${signingIdentity}" "${itemPath}"`);
            progress('fix-ext', `Re-signed nested framework: ${item}`);
        }
    }
}
// ============================================================================
// Main Export Function
// ============================================================================
/**
 * Resign DFStreamer IPA with user's provisioning profile.
 * Called via WebSocket from the hub UI.
 */
async function resignDfStreamer(options) {
    const progress = options.onProgress || (() => { });
    if (!options.provisioningUUID) {
        throw new Error('provisioningUUID is required');
    }
    if (options.isFreeAccount === undefined) {
        throw new Error('isFreeAccount is required');
    }
    progress('init', 'Starting DFStreamer resign...');
    // 1. Load dependencies
    progress('deps', 'Loading resign dependencies...');
    const { Applesign, provision } = loadDependencies();
    // 2. Find unsigned IPA (auto-extract from bundled assets if not in cache)
    progress('locate', 'Locating unsigned DFStreamer IPA...');
    const cacheDir = getCacheDir();
    ensureUnsignedIpaAvailable(cacheDir);
    const unsignedIpaPath = path_1.default.join(cacheDir, UNSIGNED_IPA_NAME);
    if (!fs_1.default.existsSync(unsignedIpaPath)) {
        throw new Error(`Unsigned DFStreamer IPA not found at ${unsignedIpaPath}. ` +
            `Please place ${UNSIGNED_IPA_NAME} in ${cacheDir}`);
    }
    progress('locate', `Found unsigned IPA: ${unsignedIpaPath}`, 10);
    // 3. Get provisioning profile
    progress('profile', 'Validating provisioning profile...');
    const { filePath: mobileProvisioningFile, profile } = await getMobileProvisioningFilePath(provision, options.provisioningUUID);
    // 4. Get bundle ID from profile and validate for extension support
    const bundleId = profile.Name.split(':')[1] || profile.Name;
    progress('profile', `Using bundle ID: ${bundleId} from profile: ${profile.Name}`, 20);
    // Check if the profile is wildcard or explicit — extensions need either a wildcard
    // profile or a separate profile matching <bundleId>.broadcast
    const mainProfileEntCheck = await extractProfileEntitlements(mobileProvisioningFile);
    if (mainProfileEntCheck) {
        const isWildcard = mainProfileEntCheck.appId.endsWith('.*') || mainProfileEntCheck.appId.endsWith('*');
        const trimmed = bundleId.replace(/^\s+|\s+$/g, '');
        const extBundleId = `${trimmed}.broadcast`;
        if (!isWildcard) {
            // Explicit profile — check if a matching extension profile exists
            const extProfile = await findProvisioningProfileForBundleId(extBundleId, mainProfileEntCheck.teamId, progress);
            if (!extProfile) {
                progress('profile', `⚠ WARNING: Profile has explicit app ID (${mainProfileEntCheck.appId}). ` +
                    `No matching profile found for extension "${extBundleId}". ` +
                    (options.isFreeAccount
                        ? `Free accounts require a separate extension profile. See error below for instructions.`
                        : `Use a WILDCARD provisioning profile (Team ID.*) to support extensions.`));
            }
            else {
                progress('profile', `Found extension profile for ${extBundleId}`, 25);
            }
        }
        else {
            progress('profile', 'Wildcard profile detected — covers main app + extensions', 25);
        }
    }
    // Resolve extension provisioning profile if UUID provided
    let extensionProvisioningFile = null;
    if (options.extensionProvisioningUUID) {
        try {
            const { filePath: extFilePath } = await getMobileProvisioningFilePath(provision, options.extensionProvisioningUUID);
            extensionProvisioningFile = extFilePath;
            progress('profile', `Extension profile: ${path_1.default.basename(extFilePath)}`, 28);
        }
        catch (err) {
            progress('profile', `⚠ Could not find extension profile UUID ${options.extensionProvisioningUUID}: ${err instanceof Error ? err.message : String(err)}`);
        }
    }
    progress('profile', `Using bundle ID: ${bundleId} from profile: ${profile.Name}`, 30);
    // 5. Resign IPA
    progress('sign', 'Resigning DFStreamer IPA...', 40);
    const outputFileName = options.outputFileName || 'dfstreamer-resign';
    const outputDir = path_1.default.join(cacheDir, 'dfstreamer-output');
    if (!fs_1.default.existsSync(outputDir)) {
        fs_1.default.mkdirSync(outputDir, { recursive: true });
    }
    const generatedIpaPath = path_1.default.join(outputDir, `${outputFileName}.ipa`);
    const trimmedBundleId = bundleId ? bundleId.replace(/^\s+|\s+$/g, '') : '';
    // Preprocess IPA: fix extension bundle IDs and provisioning profiles.
    // applesign does not handle PlugIns/*.appex bundle IDs or embedded.mobileprovision,
    // which causes installation failures for apps with broadcast extensions (ReplayKit).
    progress('preprocess', 'Checking for app extensions...', 45);
    const preprocessResult = trimmedBundleId
        ? await preprocessIpaExtensions(unsignedIpaPath, outputDir, trimmedBundleId, mobileProvisioningFile, extensionProvisioningFile, options.isFreeAccount, progress)
        : { ipaPath: unsignedIpaPath, extensionBundleIds: {} };
    const processedIpaPath = preprocessResult.ipaPath;
    const extensionBundleIds = preprocessResult.extensionBundleIds;
    // Note: do NOT pass bundleId to applesign — preprocessIpaExtensions already
    // set the main app AND extension bundle IDs. Passing bundleId to applesign
    // causes it to re-process Info.plist files and overwrite extension bundle IDs.
    const appleOptions = {
        mobileprovision: mobileProvisioningFile,
        outfile: generatedIpaPath,
    };
    const as = new Applesign(appleOptions);
    await as.signIPA(processedIpaPath);
    // Clean up intermediate modified zip if created
    if (processedIpaPath !== unsignedIpaPath && fs_1.default.existsSync(processedIpaPath)) {
        fs_1.default.unlinkSync(processedIpaPath);
    }
    progress('sign', 'DFStreamer IPA resigned successfully', 65);
    // Post-sign: fix extension bundle IDs and entitlements that applesign sets incorrectly.
    // applesign uses the main app's provisioning profile for ALL binaries, which means:
    //   1. Extension's CFBundleIdentifier is overwritten to match the main app (sub-bundle collision)
    //   2. Extension's application-identifier entitlement is set to the main app's value (wrong)
    //   3. application-groups may be stripped (correct for free accounts, but identity must match)
    // This restores correct bundle IDs and re-signs inside-out with correct per-target entitlements.
    // Failure here is FATAL — a broken extension will not install or launch on iOS.
    progress('fix-ext', 'Fixing extension bundle IDs and entitlements (inside-out re-sign)...', 67);
    await fixExtensionEntitlements(generatedIpaPath, trimmedBundleId, mobileProvisioningFile, extensionBundleIds, progress);
    progress('sign', 'DFStreamer IPA resign + extension fix complete', 70);
    // Save bundle ID for auto-launch (used by webrtcManager)
    const bundleIdFile = path_1.default.join(outputDir, 'bundleId.txt');
    fs_1.default.writeFileSync(bundleIdFile, bundleId.replace(/^\s+|\s+$/g, ''), 'utf8');
    // 6. Upload to hub if requested (default: true)
    const shouldUpload = options.uploadToHub !== false;
    let wdaId;
    if (shouldUpload) {
        const config = (0, node_config_1.getMergedConfig)();
        const hubUrl = config.hubUrl;
        const accessKey = config.accessKey;
        const secretToken = config.token;
        if (!hubUrl || !accessKey || !secretToken) {
            progress('upload', 'Hub credentials not configured, skipping upload');
        }
        else {
            const hubRunning = await isHubRunning(hubUrl);
            if (!hubRunning) {
                progress('upload', 'Hub server not reachable, skipping upload');
            }
            else {
                progress('upload', 'Uploading resigned DFStreamer to hub...', 80);
                const basicAuth = Buffer.from(`${accessKey}:${secretToken}`).toString('base64');
                const authHeader = `Basic ${basicAuth}`;
                const finalBundleId = bundleId.replace(/^\s+|\s+$/g, '');
                try {
                    const result = await uploadToHub(hubUrl, authHeader, generatedIpaPath, finalBundleId, options.setAsDefault ?? false);
                    if (result.success) {
                        wdaId = result.wdaId;
                        progress('upload', `DFStreamer uploaded successfully. ID: ${wdaId}`, 100);
                    }
                    else {
                        progress('upload', `Upload failed: ${result.message}`);
                    }
                }
                catch (uploadError) {
                    progress('upload', `Upload error: ${uploadError.message}`);
                }
            }
        }
    }
    progress('complete', 'DFStreamer resign complete!', 100);
    return {
        success: true,
        message: 'DFStreamer resign complete',
        ipaPath: generatedIpaPath,
        wdaId,
        bundleId: bundleId.replace(/^\s+|\s+$/g, ''),
    };
}
//# sourceMappingURL=prepare-dfstreamer.js.map