UNPKG

homebridge-config-ui-x

Version:

A web based management, configuration and control platform for Homebridge.

1,340 lines 65 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
var ServerService_1;
import { Buffer } from 'node:buffer';
import { exec, spawn } from 'node:child_process';
import { createPublicKey, X509Certificate } from 'node:crypto';
import { createWriteStream } from 'node:fs';
import { readdir, readFile, unlink, writeFile } from 'node:fs/promises';
import { basename, extname, join, resolve } from 'node:path';
import process from 'node:process';
import { pipeline } from 'node:stream';
import { createSecureContext } from 'node:tls';
import { promisify } from 'node:util';
import { Categories } from '@homebridge/hap-client/hap-types';
import { BadRequestException, Inject, Injectable, InternalServerErrorException, NotFoundException, ServiceUnavailableException, } from '@nestjs/common';
import { ensureDir, pathExists, readJson, remove } from 'fs-extra/esm';
import NodeCache from 'node-cache';
import { networkInterfaces } from 'systeminformation';
import { check as tcpCheck } from 'tcp-port-used';
import { ConfigService } from '../../core/config/config.service.js';
import { JsonFileStoreService } from '../../core/fs/json-file-store.service.js';
import { HomebridgeIpcService } from '../../core/homebridge-ipc/homebridge-ipc.service.js';
import { Logger } from '../../core/logger/logger.service.js';
import { RE_ACCESSORY_INFO_FILE, RE_CACHED_ACCESSORIES, RE_CACHED_ACCESSORIES_EXACT, RE_CERTIFICATE, RE_CHAR_PAIRS, RE_COLON, RE_DEVICE_ID, RE_EXTERNAL_ACCESSORIES_EXACT, RE_HEX_12, RE_HEX_ANY, RE_HYPHEN_GLOBAL, RE_PRIVATE_KEY, RE_SAFE_RESTART_CMD, RE_VALID_NAME, } from '../../core/regex.constants.js';
import { SslCertGeneratorService } from '../../core/ssl/ssl-cert-generator.service.js';
import { AccessoriesService } from '../accessories/accessories.service.js';
import { ConfigEditorService } from '../config-editor/config-editor.service.js';
const pump = promisify(pipeline);
let ServerService = ServerService_1 = class ServerService {
    configService;
    configEditorService;
    accessoriesService;
    homebridgeIpcService;
    jsonStore;
    logger;
    serverServiceCache = new NodeCache({ stdTTL: 300 });
    accessoryId;
    accessoryInfoPath;
    setupCode = null;
    paired = false;
    constructor(configService, configEditorService, accessoriesService, homebridgeIpcService, jsonStore, logger) {
        this.configService = configService;
        this.configEditorService = configEditorService;
        this.accessoriesService = accessoriesService;
        this.homebridgeIpcService = homebridgeIpcService;
        this.jsonStore = jsonStore;
        this.logger = logger;
        this.accessoryId = ServerService_1.macToHex(this.configService.homebridgeConfig.bridge.username);
        this.accessoryInfoPath = join(this.configService.storagePath, 'persist', `AccessoryInfo.${this.accessoryId}.json`);
    }
    static macToHex(mac) {
        return mac.split(':').join('').toUpperCase();
    }
    static hexToMac(hex) {
        return hex.match(RE_CHAR_PAIRS)?.join(':').toUpperCase() || hex.toUpperCase();
    }
    async deleteSingleDeviceAccessories(id, cachedAccessoriesDir, protocol = 'both') {
        if (protocol === 'hap' || protocol === 'both') {
            const cachedAccessories = resolve(cachedAccessoriesDir, `cachedAccessories.${id}`);
            const cachedAccessoriesBackup = resolve(cachedAccessoriesDir, `.cachedAccessories.${id}.bak`);
            if (cachedAccessories.startsWith(cachedAccessoriesDir) && await pathExists(cachedAccessories)) {
                await unlink(cachedAccessories);
                this.logger.warn(`Bridge ${id} HAP accessory removal: removed ${cachedAccessories}.`);
            }
            if (cachedAccessoriesBackup.startsWith(cachedAccessoriesDir) && await pathExists(cachedAccessoriesBackup)) {
                await unlink(cachedAccessoriesBackup);
                this.logger.warn(`Bridge ${id} HAP accessory removal: removed ${cachedAccessoriesBackup}.`);
            }
        }
        if (protocol === 'matter' || protocol === 'both') {
            const matterDeviceId = ServerService_1.macToHex(id);
            const matterDir = resolve(this.configService.storagePath, 'matter');
            const matterAccessoriesPath = resolve(matterDir, matterDeviceId, 'accessories.json');
            if (matterAccessoriesPath.startsWith(matterDir) && await pathExists(matterAccessoriesPath)) {
                await unlink(matterAccessoriesPath);
                this.logger.warn(`Bridge ${id} Matter accessory removal: removed ${matterAccessoriesPath}.`);
            }
        }
    }
    async deleteSingleDevicePairing(id, resetPairingInfo) {
        const persistPath = resolve(this.configService.storagePath, 'persist');
        const accessoryInfo = resolve(persistPath, `AccessoryInfo.${id}.json`);
        const identifierCache = resolve(persistPath, `IdentifierCache.${id}.json`);
        const deviceId = id.includes(':') ? ServerService_1.macToHex(id) : id.toUpperCase();
        const matterDir = resolve(this.configService.storagePath, 'matter');
        const matterPath = resolve(matterDir, deviceId);
        try {
            const configFile = await this.configEditorService.getConfigFile();
            const username = id.includes(':') ? id.toUpperCase() : ServerService_1.hexToMac(id);
            const uiConfig = configFile.platforms.find(x => x.platform === 'config');
            let blacklistChanged = false;
            let bridgesChanged = false;
            if (uiConfig.accessoryControl?.instanceBlacklist?.includes(username)) {
                blacklistChanged = true;
                uiConfig.accessoryControl.instanceBlacklist = uiConfig.accessoryControl.instanceBlacklist
                    .filter((x) => x.toUpperCase() !== username);
            }
            let oldBridgeConfig;
            if (uiConfig.bridges && Array.isArray(uiConfig.bridges)) {
                const bridgeIndex = uiConfig.bridges.findIndex(x => x.username?.toUpperCase() === username);
                if (bridgeIndex > -1) {
                    bridgesChanged = true;
                    oldBridgeConfig = uiConfig.bridges[bridgeIndex];
                    uiConfig.bridges.splice(bridgeIndex, 1);
                }
            }
            if (resetPairingInfo) {
                const pluginBlocks = [
                    ...(configFile.accessories || []),
                    ...(configFile.platforms || []),
                    { _bridge: configFile.bridge },
                ]
                    .filter((block) => block._bridge?.username?.toUpperCase() === username.toUpperCase());
                const pluginBlock = pluginBlocks.find((block) => block._bridge?.port);
                const otherBlocks = pluginBlocks.filter((block) => !block._bridge?.port);
                if (pluginBlock) {
                    pluginBlock._bridge.username = this.configEditorService.generateUsername();
                    pluginBlock._bridge.pin = this.configEditorService.generatePin();
                    otherBlocks.forEach((block) => {
                        block._bridge.username = pluginBlock._bridge.username;
                    });
                    if (blacklistChanged) {
                        uiConfig.accessoryControl.instanceBlacklist = [...uiConfig.accessoryControl.instanceBlacklist, pluginBlock._bridge.username];
                    }
                    if (bridgesChanged) {
                        uiConfig.bridges.push({
                            ...oldBridgeConfig,
                            username: pluginBlock._bridge.username,
                        });
                    }
                    this.logger.warn(`Bridge ${id} reset: new username: ${pluginBlock._bridge.username} and new pin: ${pluginBlock._bridge.pin}.`);
                }
                else {
                    this.logger.error(`Failed to reset username and pin for child bridge ${id} as the plugin block could not be found.`);
                }
            }
            if (blacklistChanged) {
                uiConfig.accessoryControl.instanceBlacklist = uiConfig.accessoryControl.instanceBlacklist
                    .sort((a, b) => a.localeCompare(b));
            }
            await this.configEditorService.updateConfigFile(configFile);
        }
        catch (e) {
            this.logger.error(`Failed to reset username and pin for child bridge ${id} as ${e.message}.`);
        }
        if (accessoryInfo.startsWith(persistPath) && await pathExists(accessoryInfo)) {
            await unlink(accessoryInfo);
            this.logger.warn(`Bridge ${id} reset: removed ${accessoryInfo}.`);
        }
        if (identifierCache.startsWith(persistPath) && await pathExists(identifierCache)) {
            await unlink(identifierCache);
            this.logger.warn(`Bridge ${id} reset: removed ${identifierCache}.`);
        }
        if (matterPath.startsWith(matterDir) && await pathExists(matterPath)) {
            await remove(matterPath);
            this.logger.warn(`Bridge ${id} reset: removed Matter bridge storage at ${matterPath}.`);
        }
        await this.deleteDeviceAccessories(id);
    }
    async restartServer() {
        this.logger.debug('Homebridge restart request received.');
        if (!await this.configService.uiRestartRequired() && !await this.nodeVersionChanged()) {
            this.logger.log('UI/Bridge settings have not changed - only restarting Homebridge process.');
            this.homebridgeIpcService.restartHomebridge();
            this.accessoriesService.resetInstancePool();
            return { ok: true, command: 'SIGTERM', restartingUI: false };
        }
        setTimeout(() => {
            const restartCmd = this.configService.ui.restart;
            if (!restartCmd) {
                this.logger.debug('Sending SIGTERM to process...');
                process.kill(process.pid, 'SIGTERM');
                return;
            }
            this.logger.log(`Executing restart command ${restartCmd}.`);
            if (this.configService.runningInDocker) {
                exec(restartCmd, (err) => {
                    if (err) {
                        this.logger.log('Restart command exited with an error, failed to restart Homebridge.');
                    }
                });
                return;
            }
            if (!RE_SAFE_RESTART_CMD.test(restartCmd)) {
                this.logger.error(`Refusing to run restart command — not on the allowlist: "${restartCmd}". Edit ui.restart to use a supported form (e.g. "sudo systemctl restart homebridge"). Falling back to SIGTERM.`);
                process.kill(process.pid, 'SIGTERM');
                return;
            }
            const argv = restartCmd.split(/\s+/).filter(Boolean);
            const child = spawn(argv[0], argv.slice(1), { stdio: 'ignore', shell: false });
            child.on('error', () => {
                this.logger.log('Restart command exited with an error, failed to restart Homebridge.');
            });
        }, 500);
        return { ok: true, command: this.configService.ui.restart, restartingUI: true };
    }
    async resetHomebridgeAccessory() {
        this.configService.hbServiceUiRestartRequired = true;
        const configFile = await this.configEditorService.getConfigFile();
        const oldUsername = configFile.bridge.username;
        configFile.bridge.pin = this.configEditorService.generatePin();
        configFile.bridge.username = this.configEditorService.generateUsername();
        const uiConfig = configFile.platforms.find(x => x.platform === 'config');
        if (uiConfig.accessoryControl?.instanceBlacklist?.includes(oldUsername.toUpperCase())) {
            uiConfig.accessoryControl.instanceBlacklist = [...uiConfig.accessoryControl.instanceBlacklist
                    .filter((x) => x.toUpperCase() !== oldUsername.toUpperCase()), configFile.bridge.username]
                .sort((a, b) => a.localeCompare(b));
        }
        this.logger.warn(`Homebridge bridge reset: new username ${configFile.bridge.username} and new pin ${configFile.bridge.pin}.`);
        await this.configEditorService.updateConfigFile(configFile);
        await remove(resolve(this.configService.storagePath, 'accessories'));
        await remove(resolve(this.configService.storagePath, 'persist'));
        const matterDir = join(this.configService.storagePath, 'matter');
        if (await pathExists(matterDir)) {
            await remove(matterDir);
            this.logger.warn('Homebridge bridge reset: removed all Matter storage.');
        }
        this.logger.log('Homebridge bridge reset: accessories, persist, and matter directories were removed.');
    }
    async getDevicePairings() {
        const persistPath = join(this.configService.storagePath, 'persist');
        const devices = (await readdir(persistPath))
            .filter(x => x.match(RE_ACCESSORY_INFO_FILE));
        const configFile = await this.configEditorService.getConfigFile();
        const externalAttribution = await this.getExternalAccessoryAttribution();
        const hapDevices = await Promise.all(devices.map(async (x) => {
            return await this.getDevicePairingById(x.split('.')[1], configFile, externalAttribution);
        }));
        const matterExternalDevices = await this.getMatterExternalAccessories(hapDevices);
        return [...hapDevices, ...matterExternalDevices].sort((a, b) => a.name.localeCompare(b.name));
    }
    async getExternalAccessoryAttribution() {
        const attribution = new Map();
        const cachedAccessoriesDir = join(this.configService.storagePath, 'accessories');
        if (!await pathExists(cachedAccessoriesDir)) {
            return attribution;
        }
        const externalFiles = (await readdir(cachedAccessoriesDir))
            .filter(x => x.match(RE_EXTERNAL_ACCESSORIES_EXACT) || x === 'externalAccessories');
        for (const file of externalFiles) {
            try {
                const entries = await readJson(join(cachedAccessoriesDir, file));
                if (!Array.isArray(entries)) {
                    continue;
                }
                for (const entry of entries) {
                    if (typeof entry?.username === 'string' && typeof entry?.plugin === 'string') {
                        attribution.set(entry.username.toUpperCase(), {
                            plugin: entry.plugin,
                            displayName: entry.displayName,
                            category: entry.category,
                            port: entry.port,
                        });
                    }
                }
            }
            catch (e) {
                this.logger.warn(`Failed to read external accessory attribution file ${file}: ${e.message}`);
            }
        }
        return attribution;
    }
    async getMatterExternalAccessories(hapDevices) {
        const matterPath = join(this.configService.storagePath, 'matter');
        if (!await pathExists(matterPath)) {
            return [];
        }
        const matterDirs = (await readdir(matterPath))
            .filter(x => x.match(RE_HEX_12));
        const matterExternalDevices = [];
        for (const deviceId of matterDirs) {
            try {
                const hasHapAccessoryInfo = hapDevices.some(d => d._id === deviceId);
                if (hasHapAccessoryInfo) {
                    continue;
                }
                const mainBridgeId = ServerService_1.macToHex(this.configService.homebridgeConfig.bridge.username);
                if (deviceId.toUpperCase() === mainBridgeId) {
                    continue;
                }
                const accessoriesPath = join(matterPath, deviceId, 'accessories.json');
                if (!await pathExists(accessoriesPath)) {
                    continue;
                }
                const accessories = await readJson(accessoriesPath);
                if (!Array.isArray(accessories) || accessories.length === 0) {
                    continue;
                }
                const accessory = accessories[0];
                const commissioningPath = join(matterPath, deviceId, 'commissioning.json');
                let commissioned = false;
                let qrCode;
                let manualPairingCode;
                if (await pathExists(commissioningPath)) {
                    try {
                        const commissioningInfo = await readJson(commissioningPath);
                        commissioned = commissioningInfo.commissioned || false;
                        qrCode = typeof commissioningInfo.qrCode === 'string' ? commissioningInfo.qrCode : undefined;
                        manualPairingCode = typeof commissioningInfo.manualPairingCode === 'string' ? commissioningInfo.manualPairingCode : undefined;
                    }
                    catch (parseError) {
                        this.logger.warn(`Malformed commissioning.json at ${commissioningPath}, removing corrupted file`);
                        try {
                            await remove(commissioningPath);
                        }
                        catch (removeError) {
                            this.logger.warn(`Failed to remove corrupted commissioning.json: ${removeError.message}`);
                        }
                    }
                }
                const device = {
                    _id: deviceId,
                    _username: ServerService_1.hexToMac(deviceId),
                    _main: false,
                    _category: 'other',
                    _matter: true,
                    _matterOnly: true,
                    _isExternal: true,
                    _isPaired: commissioned,
                    _plugin: accessory.plugin,
                    _setupCode: qrCode,
                    pincode: manualPairingCode,
                    name: accessory.displayName || 'Matter External Accessory',
                    displayName: accessory.displayName || 'Matter External Accessory',
                    manufacturer: accessory.manufacturer || 'Unknown',
                    model: accessory.model || 'Unknown',
                    serialNumber: accessory.serialNumber || deviceId,
                    category: 1,
                };
                matterExternalDevices.push(device);
            }
            catch (e) {
                this.logger.error(`Failed to read Matter external accessory ${deviceId}: ${e.message}`);
            }
        }
        return matterExternalDevices;
    }
    async getDevicePairingById(deviceId, configFile = null, externalAttribution = null) {
        const persistPath = join(this.configService.storagePath, 'persist');
        let device;
        try {
            device = await readJson(join(persistPath, `AccessoryInfo.${deviceId}.json`));
        }
        catch (e) {
            throw new NotFoundException();
        }
        if (!configFile) {
            configFile = await this.configEditorService.getConfigFile();
        }
        if (!externalAttribution) {
            externalAttribution = await this.getExternalAccessoryAttribution();
        }
        const username = ServerService_1.hexToMac(deviceId);
        const isMain = this.configService.homebridgeConfig.bridge.username.toUpperCase() === username.toUpperCase();
        const pluginBlock = [...configFile.accessories, ...configFile.platforms, { _bridge: configFile.bridge }]
            .find((block) => block._bridge?.username?.toUpperCase() === username.toUpperCase());
        try {
            device._category = Object.entries(Categories).find(([, value]) => value === device.category)[0].toLowerCase();
        }
        catch (e) {
            device._category = 'Other';
        }
        device.name = pluginBlock?._bridge.name || pluginBlock?.name || device.displayName;
        device._id = deviceId;
        device._username = username;
        device._main = isMain;
        device._isPaired = device.pairedClients && Object.keys(device.pairedClients).length > 0;
        device._setupCode = this.generateSetupCode(device);
        device._couldBeStale = !device._main && device._category === 'bridge' && !pluginBlock;
        device._matter = !!(pluginBlock?._bridge?.matter);
        const externalMeta = externalAttribution.get(username.toUpperCase());
        if (externalMeta && !isMain) {
            device._plugin = externalMeta.plugin;
            device._isExternal = true;
            if (typeof externalMeta.port === 'number') {
                device._port = externalMeta.port;
            }
            device._couldBeStale = false;
        }
        if (device._matter && pluginBlock && 'accessory' in pluginBlock) {
            this.logger.warn(`Device ${deviceId} has Matter configuration on an accessory-based plugin. Matter is only supported for platform-based plugins.`);
        }
        delete device.signSk;
        delete device.signPk;
        delete device.configHash;
        delete device.pairedClients;
        delete device.pairedClientsPermission;
        return device;
    }
    async deleteDevicePairing(id, resetPairingInfo) {
        if (!RE_DEVICE_ID.test(id)) {
            throw new BadRequestException('Invalid device ID.');
        }
        this.logger.warn(`Shutting down Homebridge before resetting paired bridge ${id}...`);
        await this.homebridgeIpcService.restartAndWaitForClose();
        await this.deleteSingleDevicePairing(id, resetPairingInfo);
        return { ok: true };
    }
    async deleteDeviceMatterConfig(id) {
        if (!RE_DEVICE_ID.test(id)) {
            throw new BadRequestException('Invalid device ID.');
        }
        this.logger.warn(`Shutting down Homebridge before removing Matter config for bridge ${id}...`);
        await this.homebridgeIpcService.restartAndWaitForClose();
        try {
            const configFile = await this.configEditorService.getConfigFile();
            const username = id.includes(':') ? id.toUpperCase() : ServerService_1.hexToMac(id);
            const pluginBlocks = [
                ...(configFile.accessories || []),
                ...(configFile.platforms || []),
            ]
                .filter((block) => block._bridge?.username?.toUpperCase() === username.toUpperCase());
            const pluginBlock = pluginBlocks.find((block) => block._bridge?.matter);
            if (!pluginBlock) {
                this.logger.warn(`Matter configuration already removed from config.json for child bridge ${id}, skipping config update.`);
            }
            else {
                if ('accessory' in pluginBlock) {
                    this.logger.warn(`Removing Matter configuration from accessory-based plugin block for bridge ${id}. Matter is only supported for platform-based plugins.`);
                }
                delete pluginBlock._bridge.matter;
                this.logger.warn(`Bridge ${id} Matter configuration removed from config.json.`);
                await this.configEditorService.updateConfigFile(configFile);
            }
        }
        catch (e) {
            this.logger.error(`Failed to remove Matter configuration for child bridge ${id} as ${e.message}.`);
            throw new InternalServerErrorException(`Failed to remove Matter configuration: ${e.message}`);
        }
        const deviceId = id.includes(':') ? ServerService_1.macToHex(id) : id.toUpperCase();
        const matterPath = join(this.configService.storagePath, 'matter', deviceId);
        if (await pathExists(matterPath)) {
            await remove(matterPath);
            this.logger.warn(`Bridge ${id} Matter storage removed at ${matterPath}.`);
        }
        return { ok: true };
    }
    async deleteDevicesPairing(bridges) {
        if (bridges.some(x => !RE_DEVICE_ID.test(x.id))) {
            throw new BadRequestException('Invalid device ID.');
        }
        this.logger.warn(`Shutting down Homebridge before resetting paired bridges ${bridges.map(x => x.id).join(', ')}...`);
        await this.homebridgeIpcService.restartAndWaitForClose();
        for (const { id, resetPairingInfo } of bridges) {
            try {
                await this.deleteSingleDevicePairing(id, resetPairingInfo);
            }
            catch (e) {
                this.logger.error(`Failed to reset paired bridge ${id} as ${e.message}.`);
            }
        }
        return { ok: true };
    }
    async deleteDeviceAccessories(id) {
        if (!RE_DEVICE_ID.test(id)) {
            throw new BadRequestException('Invalid device ID.');
        }
        this.logger.warn(`Shutting down Homebridge before removing accessories for paired bridge ${id}...`);
        await this.homebridgeIpcService.restartAndWaitForClose();
        const cachedAccessoriesDir = join(this.configService.storagePath, 'accessories');
        await this.deleteSingleDeviceAccessories(id, cachedAccessoriesDir);
    }
    async deleteDevicesAccessories(bridges) {
        if (bridges.some(x => !RE_DEVICE_ID.test(x.id))) {
            throw new BadRequestException('Invalid device ID.');
        }
        this.logger.warn(`Shutting down Homebridge before removing accessories for paired bridges ${bridges.map(x => x.id).join(', ')}...`);
        await this.homebridgeIpcService.restartAndWaitForClose();
        const cachedAccessoriesDir = join(this.configService.storagePath, 'accessories');
        for (const { id, protocol } of bridges) {
            try {
                await this.deleteSingleDeviceAccessories(id, cachedAccessoriesDir, protocol || 'both');
            }
            catch (e) {
                this.logger.error(`Failed to remove accessories for bridge ${id} as ${e.message}.`);
            }
        }
    }
    async getAccessoryOverview() {
        const [hapAccessories, matterAccessories, pairings] = await Promise.all([
            this.getCachedAccessories(),
            this.getMatterAccessories(),
            this.getDevicePairings(),
        ]);
        return { hapAccessories, matterAccessories, pairings };
    }
    async getCachedAccessories() {
        const cachedAccessoriesDir = join(this.configService.storagePath, 'accessories');
        const cachedAccessoryFiles = (await readdir(cachedAccessoriesDir))
            .filter(x => x.match(RE_CACHED_ACCESSORIES_EXACT) || x === 'cachedAccessories');
        const cachedAccessories = [];
        await Promise.all(cachedAccessoryFiles.map(async (x) => {
            const accessories = await readJson(join(cachedAccessoriesDir, x));
            for (const accessory of accessories) {
                accessory.$cacheFile = x;
                cachedAccessories.push(accessory);
            }
        }));
        return cachedAccessories;
    }
    async deleteCachedAccessory(uuid, cacheFile) {
        cacheFile = basename(cacheFile || 'cachedAccessories');
        if (cacheFile !== 'cachedAccessories' && !RE_CACHED_ACCESSORIES_EXACT.test(cacheFile)) {
            throw new BadRequestException('Invalid cache file name.');
        }
        const cachedAccessoriesPath = resolve(this.configService.storagePath, 'accessories', cacheFile);
        this.logger.warn(`Shutting down Homebridge before removing cached accessory ${uuid}...`);
        await this.homebridgeIpcService.restartAndWaitForClose();
        let found = false;
        await this.jsonStore.mutate(cachedAccessoriesPath, (current) => {
            if (current === null) {
                throw new NotFoundException();
            }
            const accessoryIndex = current.findIndex(x => x.UUID === uuid);
            if (accessoryIndex > -1) {
                current.splice(accessoryIndex, 1);
                found = true;
                this.logger.warn(`Removed cached accessory with UUID ${uuid} from file ${cacheFile}.`);
            }
            return current;
        });
        if (!found) {
            this.logger.error(`Cannot find cached accessory with UUID ${uuid} from file ${cacheFile}.`);
            throw new NotFoundException();
        }
        return { ok: true };
    }
    async deleteCachedAccessories(accessories) {
        this.logger.warn(`Shutting down Homebridge before removing cached accessories ${accessories.map(x => x.uuid).join(', ')}.`);
        await this.homebridgeIpcService.restartAndWaitForClose();
        const accessoriesByCacheFile = new Map();
        for (const { cacheFile, uuid } of accessories) {
            const accessoryCacheFile = basename(cacheFile || 'cachedAccessories');
            if (accessoryCacheFile !== 'cachedAccessories' && !RE_CACHED_ACCESSORIES_EXACT.test(accessoryCacheFile)) {
                throw new BadRequestException(`Invalid cache file name: ${accessoryCacheFile}.`);
            }
            if (!accessoriesByCacheFile.has(accessoryCacheFile)) {
                accessoriesByCacheFile.set(accessoryCacheFile, []);
            }
            accessoriesByCacheFile.get(accessoryCacheFile).push({ uuid });
        }
        for (const [cacheFile, accessories] of accessoriesByCacheFile.entries()) {
            const cachedAccessoriesPath = resolve(this.configService.storagePath, 'accessories', cacheFile);
            await this.jsonStore.mutate(cachedAccessoriesPath, (current) => {
                if (current === null) {
                    this.logger.error(`Cached accessories file not found: ${cacheFile}.`);
                    throw new NotFoundException(`Cache file ${cacheFile} not found.`);
                }
                for (const { uuid } of accessories) {
                    try {
                        const accessoryIndex = current.findIndex(x => x.UUID === uuid);
                        if (accessoryIndex > -1) {
                            current.splice(accessoryIndex, 1);
                            this.logger.warn(`Removed cached accessory with UUID ${uuid} from file ${cacheFile}.`);
                        }
                        else {
                            this.logger.error(`Cannot find cached accessory with UUID ${uuid} from file ${cacheFile}.`);
                        }
                    }
                    catch (e) {
                        this.logger.error(`Failed to remove cached accessory with UUID ${uuid} from file ${cacheFile} as ${e.message}.`);
                    }
                }
                return current;
            });
        }
        return { ok: true };
    }
    async deleteAllCachedAccessories() {
        const cachedAccessoriesDir = join(this.configService.storagePath, 'accessories');
        const cachedAccessoryPaths = (await readdir(cachedAccessoriesDir))
            .filter(x => x.match(RE_CACHED_ACCESSORIES) || x === 'cachedAccessories' || x === '.cachedAccessories.bak')
            .map(x => resolve(cachedAccessoriesDir, x));
        const cachedAccessoriesPath = resolve(this.configService.storagePath, 'accessories', 'cachedAccessories');
        await this.homebridgeIpcService.restartAndWaitForClose();
        this.logger.warn('Shutting down Homebridge before removing cached accessories');
        try {
            this.logger.log('Clearing all HAP cached accessories...');
            for (const thisCachedAccessoriesPath of cachedAccessoryPaths) {
                if (await pathExists(thisCachedAccessoriesPath)) {
                    await unlink(thisCachedAccessoriesPath);
                    this.logger.warn(`Removed ${thisCachedAccessoriesPath}.`);
                }
            }
            const matterDir = join(this.configService.storagePath, 'matter');
            if (await pathExists(matterDir)) {
                this.logger.log('Clearing all Matter cached accessories...');
                const matterBridges = (await readdir(matterDir)).filter(x => x.match(RE_HEX_12));
                for (const deviceId of matterBridges) {
                    const accessoriesPath = join(matterDir, deviceId, 'accessories.json');
                    if (await pathExists(accessoriesPath)) {
                        await unlink(accessoriesPath);
                        this.logger.warn(`Removed Matter cached accessories for bridge ${deviceId}.`);
                    }
                }
            }
        }
        catch (e) {
            this.logger.error(`Failed to clear all cached accessories at ${cachedAccessoriesPath} as ${e.message}.`);
            console.error(e);
            throw new InternalServerErrorException('Failed to clear Homebridge accessory cache - see logs.');
        }
        return { ok: true };
    }
    async getMatterAccessories() {
        const matterDir = join(this.configService.storagePath, 'matter');
        if (!await pathExists(matterDir)) {
            return [];
        }
        const matterBridges = (await readdir(matterDir))
            .filter(x => x.match(RE_HEX_ANY));
        const matterAccessories = [];
        await Promise.all(matterBridges.map(async (deviceId) => {
            try {
                const accessoriesPath = join(matterDir, deviceId, 'accessories.json');
                if (await pathExists(accessoriesPath)) {
                    const accessories = await readJson(accessoriesPath);
                    if (Array.isArray(accessories)) {
                        for (const accessory of accessories) {
                            accessory.$deviceId = deviceId;
                            accessory.$protocol = 'matter';
                            matterAccessories.push(accessory);
                        }
                    }
                }
            }
            catch (e) {
                this.logger.error(`Failed to read Matter accessories for bridge ${deviceId}: ${e.message}`);
            }
        }));
        return matterAccessories;
    }
    async deleteMatterAccessory(deviceId, uuid) {
        if (!RE_HEX_12.test(deviceId)) {
            throw new BadRequestException('Invalid device ID.');
        }
        const matterAccessoriesPath = join(this.configService.storagePath, 'matter', deviceId, 'accessories.json');
        if (!await pathExists(matterAccessoriesPath)) {
            this.logger.error(`Matter accessories file not found for bridge ${deviceId}`);
            throw new NotFoundException();
        }
        this.logger.warn(`Shutting down Homebridge before removing Matter accessory ${uuid} from bridge ${deviceId}...`);
        await this.homebridgeIpcService.restartAndWaitForClose();
        let found = false;
        await this.jsonStore.mutate(matterAccessoriesPath, (current) => {
            const matterAccessories = current ?? [];
            const accessoryIndex = matterAccessories.findIndex(x => x.uuid === uuid);
            if (accessoryIndex > -1) {
                matterAccessories.splice(accessoryIndex, 1);
                found = true;
                this.logger.warn(`Removed Matter accessory with UUID ${uuid} from bridge ${deviceId}.`);
            }
            return matterAccessories;
        }, { spaces: 2 });
        if (!found) {
            this.logger.error(`Cannot find Matter accessory with UUID ${uuid} in bridge ${deviceId}.`);
            throw new NotFoundException();
        }
        return { ok: true };
    }
    async deleteMatterAccessories(accessories) {
        if (accessories.some(({ deviceId }) => !RE_HEX_12.test(deviceId))) {
            throw new BadRequestException('Invalid device ID.');
        }
        this.logger.warn(`Shutting down Homebridge before removing Matter accessories ${accessories.map(x => x.uuid).join(', ')}.`);
        await this.homebridgeIpcService.restartAndWaitForClose();
        const accessoriesByBridge = new Map();
        for (const { deviceId, uuid } of accessories) {
            if (!accessoriesByBridge.has(deviceId)) {
                accessoriesByBridge.set(deviceId, []);
            }
            accessoriesByBridge.get(deviceId).push({ uuid });
        }
        for (const [deviceId, bridgeAccessories] of accessoriesByBridge.entries()) {
            const matterAccessoriesPath = join(this.configService.storagePath, 'matter', deviceId, 'accessories.json');
            try {
                if (!await pathExists(matterAccessoriesPath)) {
                    this.logger.error(`Matter accessories file not found for bridge ${deviceId}`);
                    continue;
                }
                await this.jsonStore.mutate(matterAccessoriesPath, (current) => {
                    const matterAccessories = current ?? [];
                    for (const { uuid } of bridgeAccessories) {
                        try {
                            const accessoryIndex = matterAccessories.findIndex(x => x.uuid === uuid);
                            if (accessoryIndex > -1) {
                                matterAccessories.splice(accessoryIndex, 1);
                                this.logger.warn(`Removed Matter accessory with UUID ${uuid} from bridge ${deviceId}.`);
                            }
                            else {
                                this.logger.error(`Cannot find Matter accessory with UUID ${uuid} in bridge ${deviceId}.`);
                            }
                        }
                        catch (e) {
                            this.logger.error(`Failed to remove Matter accessory with UUID ${uuid} from bridge ${deviceId} as ${e.message}.`);
                        }
                    }
                    return matterAccessories;
                }, { spaces: 2 });
            }
            catch (e) {
                this.logger.error(`Failed to process Matter accessories for bridge ${deviceId} as ${e.message}.`);
            }
        }
        return { ok: true };
    }
    async getSetupCode() {
        if (this.setupCode) {
            return this.setupCode;
        }
        else {
            if (!await pathExists(this.accessoryInfoPath)) {
                return null;
            }
            const accessoryInfo = await readJson(this.accessoryInfoPath);
            this.setupCode = this.generateSetupCode(accessoryInfo);
            return this.setupCode;
        }
    }
    generateSetupCode(accessoryInfo) {
        const buffer = Buffer.allocUnsafe(8);
        let valueLow = Number.parseInt(accessoryInfo.pincode.replace(RE_HYPHEN_GLOBAL, ''), 10);
        const valueHigh = accessoryInfo.category >> 1;
        valueLow |= 1 << 28;
        buffer.writeUInt32BE(valueLow, 4);
        if (accessoryInfo.category & 1) {
            buffer[4] = buffer[4] | 1 << 7;
        }
        buffer.writeUInt32BE(valueHigh, 0);
        let encodedPayload = (buffer.readUInt32BE(4) + (buffer.readUInt32BE(0) * 2 ** 32)).toString(36).toUpperCase();
        if (encodedPayload.length !== 9) {
            for (let i = 0; i <= 9 - encodedPayload.length; i += 1) {
                encodedPayload = `0${encodedPayload}`;
            }
        }
        return `X-HM://${encodedPayload}${accessoryInfo.setupID}`;
    }
    async getBridgePairingInformation() {
        if (!await pathExists(this.accessoryInfoPath)) {
            throw new ServiceUnavailableException('Pairing Information Not Available Yet');
        }
        const accessoryInfo = await readJson(this.accessoryInfoPath);
        return {
            displayName: accessoryInfo.displayName,
            pincode: accessoryInfo.pincode,
            setupCode: await this.getSetupCode(),
            isPaired: accessoryInfo.pairedClients && Object.keys(accessoryInfo.pairedClients).length > 0,
        };
    }
    async getSystemNetworkInterfaces() {
        const fromCache = this.serverServiceCache.get('network-interfaces');
        const interfaces = fromCache || (await networkInterfaces()).filter((adapter) => {
            return !adapter.internal
                && (adapter.ip4 || (adapter.ip6));
        });
        if (!fromCache) {
            this.serverServiceCache.set('network-interfaces', interfaces);
        }
        return interfaces;
    }
    async getHomebridgeNetworkInterfaces() {
        const config = await this.configEditorService.getConfigFile();
        if (!config.bridge?.bind) {
            return [];
        }
        if (Array.isArray(config.bridge?.bind)) {
            return config.bridge.bind;
        }
        if (typeof config.bridge?.bind === 'string') {
            return [config.bridge.bind];
        }
        return [];
    }
    async getHomebridgeMdnsSetting() {
        const config = await this.configEditorService.getConfigFile();
        if (!config.bridge.advertiser) {
            config.bridge.advertiser = 'bonjour-hap';
        }
        return {
            advertiser: config.bridge.advertiser,
        };
    }
    async setHomebridgeMdnsSetting(setting) {
        const config = await this.configEditorService.getConfigFile();
        config.bridge.advertiser = setting.advertiser;
        await this.configEditorService.updateConfigFile(config);
    }
    async setHomebridgeNetworkInterfaces(adapters) {
        const config = await this.configEditorService.getConfigFile();
        if (!config.bridge) {
            config.bridge = {};
        }
        if (!adapters.length) {
            delete config.bridge.bind;
        }
        else {
            config.bridge.bind = adapters;
        }
        await this.configEditorService.updateConfigFile(config);
    }
    async lookupUnusedPort() {
        const min = this.configService.homebridgeConfig.ports?.start ?? 30000;
        const max = this.configService.homebridgeConfig.ports?.end ?? 60000;
        const randomPort = () => Math.floor(Math.random() * (max - min + 1) + min);
        let port = randomPort();
        while (await tcpCheck(port)) {
            port = randomPort();
        }
        return { port };
    }
    async lookupUnusedMatterPort() {
        const config = await this.configEditorService.getConfigFile();
        const min = config.matterPorts?.start ?? 5530;
        const max = config.matterPorts?.end ?? 5541;
        const usedMatterPorts = new Set();
        if (config.bridge?.matter?.port) {
            usedMatterPorts.add(config.bridge.matter.port);
        }
        for (const block of [...(config.accessories || []), ...(config.platforms || [])]) {
            if (block._bridge?.matter?.port) {
                if ('accessory' in block) {
                    this.logger.warn(`Found Matter configuration on accessory-based plugin block, skipping port ${block._bridge.matter.port}`);
                    continue;
                }
                usedMatterPorts.add(block._bridge.matter.port);
            }
        }
        for (let port = min; port <= max; port += 1) {
            if (!usedMatterPorts.has(port) && !await tcpCheck(port)) {
                return { port };
            }
        }
        throw new InternalServerErrorException(`No available ports in the Matter port range (${min}-${max})`);
    }
    async getHomebridgePort() {
        const config = await this.configEditorService.getConfigFile();
        return { port: config.bridge.port };
    }
    async getUsablePorts() {
        const config = await this.configEditorService.getConfigFile();
        let start;
        let end;
        if (config.ports && typeof config.ports === 'object') {
            if (config.ports.start) {
                start = config.ports.start;
            }
            if (config.ports.end) {
                end = config.ports.end;
            }
        }
        return { start, end };
    }
    async setHomebridgeName(name) {
        if (!name || !RE_VALID_NAME.test(name)) {
            throw new BadRequestException('Invalid name');
        }
        const config = await this.configEditorService.getConfigFile();
        config.bridge.name = name;
        await this.configEditorService.updateConfigFile(config);
    }
    async setHomebridgePort(port) {
        if (!port || typeof port !== 'number' || !Number.isInteger(port) || port < 1025 || port > 65533) {
            throw new BadRequestException('Invalid port number');
        }
        const config = await this.configEditorService.getConfigFile();
        config.bridge.port = port;
        await this.configEditorService.updateConfigFile(config);
    }
    async setUsablePorts(value) {
        let config = await this.configEditorService.getConfigFile();
        if (value.start === null) {
            delete value.start;
        }
        if (value.end === null) {
            delete value.end;
        }
        if ('start' in value && (typeof value.start !== 'number' || value.start < 1025 || value.start > 65533)) {
            throw new BadRequestException('Port start must be a number between 1025 and 65533.');
        }
        if ('end' in value && (typeof value.end !== 'number' || value.end < 1025 || value.end > 65533)) {
            throw new BadRequestException('Port end must be a number between 1025 and 65533.');
        }
        if ('start' in value && 'end' in value && value.start >= value.end) {
            throw new BadRequestException('Ports start must be less than end.');
        }
        if ('start' in value && !('end' in value) && config.ports?.end && value.start >= config.ports.end) {
            throw new BadRequestException('Ports start must be less than end.');
        }
        if ('end' in value && !('start' in value) && config.ports?.start && config.ports.start >= value.end) {
            throw new BadRequestException('Ports start must be less than end.');
        }
        if (!value.start && !value.end) {
            delete config.ports;
        }
        else {
            config.ports = {};
            if (value.start) {
                config.ports.start = value.start;
            }
            if (value.end) {
                config.ports.end = value.end;
            }
        }
        const { bridge, ports, ...rest } = config;
        config = ports ? { bridge, ports, ...rest } : { bridge, ...rest };
        await this.configEditorService.updateConfigFile(config);
    }
    async getNetworkOverview() {
        const config = await this.configEditorService.getConfigFile();
        const entries = [];
        const portMap = new Map();
        const matterDir = join(this.configService.storagePath, 'matter');
        const trackPort = (port, label, protocol) => {
            if (!port) {
                return;
            }
            if (!portMap.has(port)) {
                portMap.set(port, []);
            }
            portMap.get(port).push(`${label} (${protocol})`);
        };
        const readMatterDiagnostics = async (username) => {
            const deviceId = username.replace(RE_COLON, '').toUpperCase();
            let commissioned = false;
            let deviceCount = 0;
            if (await pathExists(matterDir)) {
                const commissioningPath = join(matterDir, deviceId, 'commissioning.json');
                if (await pathExists(commissioningPath)) {
                    try {
                        const info = await readJson(commissioningPath);
                        commissioned = info.commissioned || false;
                    }
                    catch {
                    }
                }
                const accessoriesPath = join(matterDir, deviceId, 'accessories.json');
                if (await pathExists(accessoriesPath)) {
                    try {
                        const accessories = await readJson(accessoriesPath);
                        deviceCount = Array.isArray(accessories) ? accessories.length : 0;
                    }
                    catch {
                    }
                }
            }
            return { commissioned, deviceCount };
        };
        const mainBridgeName = config.bridge.name || 'Homebridge';
        const mainEntry = {
            service: 'Homebridge',
            port: config.bridge.port,
            protocol: 'HAP',
            bridge: mainBridgeName,
            status: 'ok',
        };
        trackPort(config.bridge.port, mainBridgeName, 'HAP');
        if (config.bridge.matter?.port) {
            mainEntry.matterPort = config.bridge.matter.port;
            trackPort(config.bridge.matter.port, mainBridgeName, 'Matter');
            const diag = await readMatterDiagnostics(config.bridge.username);
            mainEntry.commissioned = diag.commissioned;
            mainEntry.deviceCount = diag.deviceCount;
        }
        entries.push(mainEntry);
        for (const block of [...(config.accessories || []), ...(config.platforms || [])]) {
            if (block._bridge) {
                const bridgeName = block._bridge.name || block.name || ('platform' in block ? block.platform : block.accessory);
                const entry = {
                    service: bridgeName,
                    port: block._bridge.port || 0,
                    protocol: 'HAP',
                    bridge: bridgeName,
                    status: 'ok',
                };
                if (block._bridge.port) {
                    trackPort(block._bridge.port, bridgeName, 'HAP');
                }
                if (block._bridge.matter?.port && !('accessory' in block)) {
                    entry.matterPort = block._bridge.matter.port;
                    trackPort(block._bridge.matter.port, bridgeName, 'Matter');
                    const diag = await readMatterDiagnostics(block._bridge.username);
                    entry.commissioned = diag.commissioned;
                    entry.deviceCount = diag.deviceCount;
                }
                entries.push(entry);
            }
        }
        const uiConfig = config.platforms?.find(x => x.platform === 'config');
        const uiPort = uiConfig?.port || this.configService.ui.port;
        entries.push({
            service: 'Config UI',
            port: uiPort,
            protocol: 'UI',
            bridge: 'UI',
            status: 'ok',
        });
        trackPort(uiPort, 'UI', 'UI');
        const conflicts = [];
        for (const [port, services] of portMap) {
            if (services.length > 1) {
                conflicts.push(`Port ${port} is used by: ${services.join(', ')}`);
            }
        }
        for (const entry of entries) {
            if (portMap.get(entry.port)?.length > 1) {
                entry.status = 'conflict';
            }
            if (entry.matterPort && portMap.get(entry.matterPort)?.length > 1) {
                entry.status = 'conflict';
            }
        }
        return { entries, conflicts };
    }
    async uploadWallpaper(data) {
        const configFile = await this.configEditorService.getConfigFile();
        const uiConfigBlock = configFile.platforms.find(x => x.platform === 'config');
        if (uiConfigBlock) {
            if (uiConfigBlock.wallpaper) {
                const oldPath = join(this.configService.storagePath, uiConfigBlock.wallpaper);
                if (await pathExists(oldPath)) {
                    try {
                        await unlink(oldPath);
                        this.logger.log(`Old wallpaper file ${oldPath} deleted successfully.`);
                    }
                    catch (e) {
                        this.logger.error(`Failed to delete old wallpaper ${oldPath} as ${e.message}.`);
                    }
                }
            }
            const fileExtension = extname(data.filename);
            const newPath = join(this.configService.storagePath, `ui-wallpaper${fileExtension}`);
            await pump(data.file, createWriteStream(newPath));
            uiConfigBlock.wallpaper = `ui-wallpaper${fileExtension}`;
            await this.configEditorService.updateConfigFile(configFile);
            this.logger.log('Wallpaper uploaded and set in the config file.');
        }
    }
    async deleteWallpaper() {
        const configFile = await this.configEditorService.getConfigFile();
        const uiConfigBlock = configFile.platforms.find(x => x.platform === 'config');
        const fullPath = join(this.configService.storagePath, uiConfigBlock.wallpaper);
        if (uiConfigBlock && uiConfigBlock.wallpaper) {
            if (await pathExists(fullPath)) {
                try {
                    await unlink(fullPath);
                    this.logger.log(`Wallpaper file ${uiConfigBlock.wallpaper} deleted successfully.`);
                }
                catch (e) {
                    this.logger.error(`Failed to delete wallpaper file (${uiConfigBlock.wallpaper}) as ${e.message}.`);
                }
            }
            delete uiConfigBlock.wallpaper;
            await this.configEditorService.updateConfigFile(configFile);
            this.configService.removeWallpaperCache();
            this.logger.log('Wallpaper reference removed from the config file.');
        }
    }
    async nodeVersionChanged() {
        return new Promise((res) => {
            let result = false;
            const child = spawn(process.execPath, ['-v']);
            child.stdout.once('data', (data) => {
                result = data.toString().trim() !== process.version;
            });
            child.on('error', () => {
                result = true;
            });
            child.on('close', () => {
                return res(result);
            });
        });
    }
    async uploadSslKeyCert(req) {
        const parts = req.parts ? req.parts({ limits: { files: 2 } }) : null;
        const files = [];
        if (parts) {
            for await (const part of parts) {
                if (part.file) {
                    files.push(part);
                }
            }
        }
        else {
            const single = await req.file();
            if (single?.file) {
                files.push(single);
            }
        }
        if (!files.length) {
            throw new BadRequestException('No files uploaded. Please upload both the private key and certificate files.');
        }
        const readStreamToBuffer = async (stream) => {
            const chunks = [];
            await new Promise((resolvePromise, rejectPromise) => {
                stream.on('data', (d) => chunks.push(Buffer.isBuffer(d) ? d : Buffer.from(d)));
                stream.on('end', () => resolvePromise());
                stream.on('error', rejectPromise);
            });
            return Buffer.concat(chunks);
        };
        let keyPem = null;
        let certPem = null;
        for (const f of files) {
            if (f.file?.truncated) {
                throw new InternalServerErrorException(`Upload exceeds maximum size ${globalThis.backup.maxBackupSizeText}.`);
            }
            const buf = await readStreamToBuffer(f.file);
            const text = buf.toString('utf8');
            const hasKey = RE_PRIVATE_KEY.test(text);
            const hasCert = RE_CERTIFICATE.test(text);
            if (hasKey && !keyPem) {
                keyPem = buf;
            }
            if (hasCert && !certPem) {
                certPem = buf;
            }
            if (!hasKey && !hasCert) {
                if (f.fieldname === 'key' && !keyPem) {
                    keyPem = buf;
                }
                else if (f.fieldname === 'cert' && !certPem) {
                    certPem = buf;
                }
            }
        }
        if (!keyPem || !certPem) {
            throw new BadRequestException('Both a PEM private key and certificate must be provided.');
        }
        try {
            const x509 = new X509Certificate(certPem);
            const certPub = x509.publicKey.export({ type: 'spki', format: 'der' });
            const pubFromPriv = createPublicKey(keyPem).export({ type: 'spki', format: 'der' });
            if (!certPub.equals(pubFromPriv)) {
                throw new BadRequestException('The private key does not match the certificate public key.');
            }
            createSecureContext({ key: keyPem, cert: certPem });
        }
        catch (e) {
            if (e instanceof BadRequestException) {
                throw e;
            }
            throw new BadRequestException(`Invalid key/certificate: ${e?.message || e}`);
        }
        const sslDir = join(this.configService.storagePath, 'ssl-certs');
        const keyPath = join(sslDir, 'ui-ssl.key');
        const certPath = join(sslDir, 'ui-ssl.crt');
        await ensureDir(sslDir);
        await writeFile(keyPath, keyPem);
        await writeFile(certPath, certPem);
        const configFile = await this.configEditorService.getConfigFile();
        const uiConfigBlock = configFile.platforms.find((x) => x.platform === 'config');
        if (!uiConfigBlock) {
            throw new InternalServerErrorException('Config platform block not found.');
        }
        if (!uiConfigBlock.ssl) {
            uiConfigBlock.ssl = {};
        }
        uiConfigBlock.ssl.key = keyPath;
        uiConfigBlock.ssl.cert = certPath;
        delete uiConfigBlock.ssl.pfx;
        delete uiConfigBlock.ssl.passphrase;
        uiConfigBlock.ssl.selfSigned = false;
        await this.configEditorService.updateConfigFile(configFile);
        return {
            ok: true,
            type: 'keycert',
            keyPath,
            certPath,
            details: 'Certificate and key validated and saved.',
        };
    }
    async uploadSslPfx(req) {
        let passphrase;
        let filePart;
        if (req.parts) {
            for await (const part of req.parts()) {
                if (part.type === 'file' || part.file) {
                    filePart = part;
                }
                else if (part.type === 'field' || part.value) {
                    if (part.fieldname === 'passphrase') {
                        passphrase = part.value;
                    }
                }
            }
        }
        else {
            filePart = await req.file();
            passphrase = req.body?.passphrase;
        }
        if (!filePart) {
            throw new BadRequestException('No PFX file uploaded.');
        }
        if (filePart.file?.truncated) {
            throw new InternalServerErrorException(`Upload exceeds maximum size ${globalThis.backup.maxBackupSizeText}.`);
        }
        const readStreamToBuffer = async (stream) => {
            const chunks = [];
            await new Promise((resolvePromise, rejectPromise) => {
                stream.on('data', (d) => chunks.push(Buffer.isBuffer(d) ? d : Buffer.from(d)));
                stream.on('end', () => resolvePromise());
                stream.on('error', rejectPromise);
            });
            return Buffer.concat(chunks);
        };
        const pfxBuffer = await readStreamToBuffer(filePart.file);
        try {
            createSecureContext({ pfx: pfxBuffer, passphrase });
        }
        catch (e) {
            throw new BadRequestException(`Invalid PFX or passphrase: ${e?.message || e}`);
        }
        const sslDir = join(this.configService.storagePath, 'ssl-certs');
        const pfxPath = join(sslDir, 'ui-ssl.pfx');
        await ensureDir(sslDir);
        await writeFile(pfxPath, pfxBuffer);
        const configFile = await this.configEditorService.getConfigFile();
        const uiConfigBlock = configFile.platforms.find((x) => x.platform === 'config');
        if (!uiConfigBlock) {
            throw new InternalServerErrorException('Config platform block not found.');
        }
        if (!uiConfigBlock.ssl) {
            uiConfigBlock.ssl = {};
        }
        uiConfigBlock.ssl.pfx = pfxPath;
        uiConfigBlock.ssl.passphrase = passphrase || '';
        delete uiConfigBlock.ssl.key;
        delete uiConfigBlock.ssl.cert;
        uiConfigBlock.ssl.selfSigned = false;
        await this.configEditorService.updateConfigFile(configFile);
        return {
            ok: true,
            type: 'pfx',
            pfxPath,
            details: 'PFX validated and saved.',
        };
    }
    async validateCurrentSslConfig() {
        const configFile = await this.configEditorService.getConfigFile();
        const uiConfigBlock = configFile.platforms.find((x) => x.platform === 'config');
        const ssl = uiConfigBlock?.ssl || {};
        if (!ssl || (!ssl.selfSigned && !ssl.key && !ssl.cert && !ssl.pfx)) {
            return { ok: true, valid: true, type: 'off', details: 'HTTPS is disabled.' };
        }
        if (ssl.selfSigned) {
            return { ok: true, valid: true, type: 'selfsigned', details: 'Self-signed mode enabled.' };
        }
        try {
            if (ssl.key && ssl.cert) {
                const keyPem = await readFile(ssl.key);
                const certPem = await readFile(ssl.cert);
                const x509 = new X509Certificate(certPem);
                const certPub = x509.publicKey.export({ type: 'spki', format: 'der' });
                const pubFromPriv = createPublicKey(keyPem).export({ type: 'spki', format: 'der' });
                if (!certPub.equals(pubFromPriv)) {
                    return { ok: true, valid: false, type: 'keycert', details: 'Private key does not match certificate.' };
                }
                createSecureContext({ key: keyPem, cert: certPem });
                return { ok: true, valid: true, type: 'keycert', details: 'Key and certificate are valid and match.' };
            }
            if (ssl.pfx) {
                const pfx = await readFile(ssl.pfx);
                createSecureContext({ pfx, passphrase: ssl.passphrase });
                return { ok: true, valid: true, type: 'pfx', details: 'PFX file and passphrase are valid.' };
            }
        }
        catch (e) {
            return { ok: true, valid: false, type: ssl.pfx ? 'pfx' : 'keycert', details: e?.message || String(e) };
        }
        return { ok: true, valid: false, type: 'off', details: 'No SSL configuration found.' };
    }
    async generateSelfSignedCertificate(options = {}) {
        const hostnames = Array.isArray(options.hostnames) && options.hostnames.length
            ? options.hostnames.map(h => String(h).trim()).filter(Boolean)
            : ['localhost', '127.0.0.1'];
        const mode = options.mode || 'keycert';
        const generator = new SslCertGeneratorService();
        await generator.generateCertificate(hostnames);
        const sslDir = join(this.configService.storagePath, 'ssl-certs');
        const keyPath = join(sslDir, 'private-key.pem');
        const certPath = join(sslDir, 'certificate.pem');
        const configFile = await this.configEditorService.getConfigFile();
        const uiConfigBlock = configFile.platforms.find((x) => x.platform === 'config');
        if (!uiConfigBlock.ssl) {
            uiConfigBlock.ssl = {};
        }
        if (mode === 'keycert') {
            uiConfigBlock.ssl.key = keyPath;
            uiConfigBlock.ssl.cert = certPath;
            delete uiConfigBlock.ssl.pfx;
            delete uiConfigBlock.ssl.passphrase;
            delete uiConfigBlock.ssl.selfSigned;
            delete uiConfigBlock.ssl.selfSignedHostnames;
        }
        else {
            delete uiConfigBlock.ssl.key;
            delete uiConfigBlock.ssl.cert;
            delete uiConfigBlock.ssl.pfx;
            delete uiConfigBlock.ssl.passphrase;
            uiConfigBlock.ssl.selfSigned = true;
            uiConfigBlock.ssl.selfSignedHostnames = hostnames;
        }
        await this.configEditorService.updateConfigFile(configFile);
        return {
            ok: true,
            type: 'generated',
            mode,
            keyPath: mode === 'keycert' ? keyPath : undefined,
            certPath: mode === 'keycert' ? certPath : undefined,
            details: `Self-signed certificate generated for ${hostnames.join(', ')}`,
        };
    }
};
ServerService = ServerService_1 = __decorate([
    Injectable(),
    __param(0, Inject(ConfigService)),
    __param(1, Inject(ConfigEditorService)),
    __param(2, Inject(AccessoriesService)),
    __param(3, Inject(HomebridgeIpcService)),
    __param(4, Inject(JsonFileStoreService)),
    __param(5, Inject(Logger)),
    __metadata("design:paramtypes", [ConfigService,
        ConfigEditorService,
        AccessoriesService,
        HomebridgeIpcService,
        JsonFileStoreService,
        Logger])
], ServerService);
export { ServerService };
//# sourceMappingURL=server.service.js.map