"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.DEFAULT_CONFIG = void 0;
exports.encryptPassword = encryptPassword;
exports.decryptPassword = decryptPassword;
exports.isValidKeyPath = isValidKeyPath;
exports.sanitizeFolderName = sanitizeFolderName;
exports.getSyncConfig = getSyncConfig;
exports.setSyncConfig = setSyncConfig;
exports.listUsersWithSyncConfig = listUsersWithSyncConfig;
exports.publicConfig = publicConfig;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const crypto = __importStar(require("crypto"));
const config_1 = require("./config");
const DEFAULT_EXCLUDES = [
    '.git/', // full .git (prev only excluded objects/, which left a broken repo)
    'node_modules/',
    'dist/',
    'build/',
    '.next/',
    '.venv/',
    '__pycache__/',
    '.DS_Store',
    '*.log',
    '*.tmp',
];
exports.DEFAULT_CONFIG = {
    host: '',
    port: 22,
    user: '',
    authMethod: 'key',
    remoteRoot: '',
    direction: 'push',
    defaultExcludes: DEFAULT_EXCLUDES,
    schedule: { enabled: false, cron: '0 3 * * *' },
    projectExcludes: {},
};
const SYNC_DIR = path.join(config_1.DATA_DIR, 'sync-config');
function ensureDir() {
    if (!fs.existsSync(SYNC_DIR))
        fs.mkdirSync(SYNC_DIR, { recursive: true, mode: 0o700 });
}
function userFile(username) {
    // Hash-based filenames: usernames may contain any unicode. sha1 is fine
    // here because this is a lookup key, not a security token.
    const hash = crypto.createHash('sha1').update(`ccweb-sync-user:${username}`).digest('hex');
    return path.join(SYNC_DIR, `${hash}.json`);
}
// ── Crypto ───────────────────────────────────────────────────────────────────
function getKey() {
    const secret = (0, config_1.getConfig)().jwtSecret;
    return crypto.createHash('sha256').update(`ccweb-sync:${secret}`).digest();
}
function keyFingerprint() {
    return getKey().subarray(0, 4).toString('hex');
}
function encryptPassword(plain) {
    if (!plain)
        return { enc: '', fp: '' };
    const iv = crypto.randomBytes(12);
    const cipher = crypto.createCipheriv('aes-256-gcm', getKey(), iv);
    const enc = Buffer.concat([cipher.update(plain, 'utf8'), cipher.final()]);
    const tag = cipher.getAuthTag();
    return {
        enc: Buffer.concat([iv, tag, enc]).toString('base64'),
        fp: keyFingerprint(),
    };
}
function decryptPassword(blob, expectedFp) {
    if (!blob)
        return '';
    if (expectedFp && expectedFp !== keyFingerprint())
        return ''; // key rotated — refuse
    try {
        const buf = Buffer.from(blob, 'base64');
        if (buf.length < 28)
            return '';
        const iv = buf.subarray(0, 12);
        const tag = buf.subarray(12, 28);
        const enc = buf.subarray(28);
        const decipher = crypto.createDecipheriv('aes-256-gcm', getKey(), iv);
        decipher.setAuthTag(tag);
        return Buffer.concat([decipher.update(enc), decipher.final()]).toString('utf8');
    }
    catch {
        return '';
    }
}
// ── Validation ───────────────────────────────────────────────────────────────
/** Reject paths that would let a user inject ssh options through rsync's
 *  `-e` string tokenization or ssh's own getopt (keyPath beginning with `-`
 *  is read as another option). */
const INVALID_KEYPATH = /[\s'"\\\x00]|^-/;
function isValidKeyPath(p) {
    if (!p)
        return true; // missing is OK (caller decides whether required)
    return !INVALID_KEYPATH.test(p) && p.length < 512;
}
/** A project folder name used on the remote must not contain path separators
 *  or start with `.`/`-` so `rsync ... remote:root/<name>/` stays inside
 *  `remoteRoot`.  `path.posix.join` does not protect against `..`. */
function sanitizeFolderName(raw) {
    if (!raw)
        return null;
    if (raw.includes('/') || raw.includes('\\') || raw.includes('\0'))
        return null;
    if (raw === '.' || raw === '..')
        return null;
    if (raw.startsWith('.') || raw.startsWith('-'))
        return null;
    if (raw.length > 128)
        return null;
    return raw;
}
// ── Read / Write ─────────────────────────────────────────────────────────────
function getSyncConfig(username) {
    ensureDir();
    const file = userFile(username);
    if (!fs.existsSync(file))
        return { username, ...exports.DEFAULT_CONFIG };
    try {
        const raw = fs.readFileSync(file, 'utf-8');
        const parsed = JSON.parse(raw);
        // username comes from the argument (authoritative) — if the file's
        // `username` field is missing / stale / tampered, the argument wins.
        const { username: _ignored, ...rest } = parsed;
        return { ...exports.DEFAULT_CONFIG, ...rest, username };
    }
    catch {
        return { username, ...exports.DEFAULT_CONFIG };
    }
}
function setSyncConfig(cfg) {
    ensureDir();
    (0, config_1.atomicWriteSync)(userFile(cfg.username), JSON.stringify(cfg, null, 2));
    try {
        fs.chmodSync(userFile(cfg.username), 0o600);
    }
    catch { /* ignore */ }
}
function listUsersWithSyncConfig() {
    ensureDir();
    try {
        const files = fs.readdirSync(SYNC_DIR).filter((f) => f.endsWith('.json'));
        const users = [];
        for (const f of files) {
            try {
                const raw = fs.readFileSync(path.join(SYNC_DIR, f), 'utf-8');
                const obj = JSON.parse(raw);
                if (typeof obj.username === 'string' && obj.username)
                    users.push(obj.username);
            }
            catch { /* skip corrupt entries */ }
        }
        return users;
    }
    catch {
        return [];
    }
}
/**
 * Client-safe projection. Never leaks the password ciphertext. Sets
 * `passwordNeedsReset` when the stored ciphertext was encrypted with a key
 * the server no longer holds (jwtSecret rotated) — the UI should prompt for
 * re-entry instead of silently rendering `passwordSet: true`.
 */
function publicConfig(cfg) {
    const { passwordEnc, passwordFp, ...rest } = cfg;
    const set = !!passwordEnc;
    const wedged = set && !!passwordFp && passwordFp !== keyFingerprint();
    return { ...rest, passwordSet: set && !wedged, passwordNeedsReset: wedged };
}
//# sourceMappingURL=sync-config.js.map