"use strict";
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;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.notifyService = void 0;
exports.getNotifyConfig = getNotifyConfig;
exports.saveNotifyConfig = saveNotifyConfig;
exports.isPrivateAddress = isPrivateAddress;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const dns = __importStar(require("dns/promises"));
const config_1 = require("./config");
const events_1 = require("events");
const logger_1 = require("./logger");
const log = (0, logger_1.modLogger)('notify');
const NOTIFY_CONFIG_FILE = path.join(config_1.DATA_DIR, 'notify-config.json');
function getNotifyConfig() {
    try {
        if (!fs.existsSync(NOTIFY_CONFIG_FILE))
            return { webhookEnabled: false };
        return JSON.parse(fs.readFileSync(NOTIFY_CONFIG_FILE, 'utf-8'));
    }
    catch {
        return { webhookEnabled: false };
    }
}
function saveNotifyConfig(config) {
    const tmpPath = NOTIFY_CONFIG_FILE + `.tmp.${process.pid}`;
    fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2), 'utf-8');
    fs.renameSync(tmpPath, NOTIFY_CONFIG_FILE);
}
function isPrivateAddress(addr) {
    const a = addr.replace(/^\[|\]$/g, '').toLowerCase();
    if (a === 'localhost' || a === '0.0.0.0' || a === '127.0.0.1' || a === '::1')
        return true;
    if (a.startsWith('10.') || a.startsWith('192.168.'))
        return true;
    if (/^172\.(1[6-9]|2\d|3[01])\./.test(a))
        return true;
    if (a.startsWith('169.254.'))
        return true; // link-local
    if (a.startsWith('fe80') || a.startsWith('fc') || a.startsWith('fd'))
        return true; // ULA / link-local v6
    // IPv4-mapped IPv6 (::ffff:a.b.c.d) — extract the embedded v4 and re-test,
    // catching ::ffff:172.16.x.x / ::ffff:169.254.x.x SSRF bypasses that the
    // earlier prefix-only check missed.
    const mapped = a.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
    if (mapped && isPrivateAddress(mapped[1]))
        return true;
    if (a === 'metadata.google.internal')
        return true;
    return false;
}
async function isWebhookUrlSafe(rawUrl) {
    let parsed;
    try {
        parsed = new URL(rawUrl);
    }
    catch {
        return false;
    }
    if (!['http:', 'https:'].includes(parsed.protocol))
        return false;
    const host = parsed.hostname.replace(/^\[|\]$/g, '');
    // Reject obvious literal-private hosts before the DNS roundtrip.
    if (isPrivateAddress(host))
        return false;
    // Resolve DNS so a public hostname pointing at RFC1918 / loopback is also rejected.
    // A literal IP just resolves to itself; an attacker hostname `whatever.example.com`
    // with an A record of `192.168.0.1` is now blocked too.
    try {
        const records = await dns.lookup(host, { all: true, verbatim: true });
        for (const r of records) {
            if (isPrivateAddress(r.address))
                return false;
        }
    }
    catch {
        // DNS failure: refuse rather than fetch a host we can't classify.
        return false;
    }
    return true;
}
class NotifyService extends events_1.EventEmitter {
    async onProjectStopped(projectId, projectName) {
        this.emit('stopped', { projectId, projectName });
        const config = getNotifyConfig();
        if (!config.webhookEnabled || !config.webhookUrl)
            return;
        // SSRF guard: literal-private + DNS-resolved private both rejected.
        if (!(await isWebhookUrlSafe(config.webhookUrl)))
            return;
        try {
            await fetch(config.webhookUrl, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    event: 'project_stopped',
                    projectId,
                    projectName,
                    timestamp: new Date().toISOString(),
                }),
                signal: AbortSignal.timeout(5000),
            });
        }
        catch (err) {
            log.warn({ err }, 'webhook delivery failed');
        }
    }
}
exports.notifyService = new NotifyService();
//# sourceMappingURL=notify-service.js.map