"use strict";
/*
 * ccweb 结构化日志（pino + pino-roll + ALS）
 *
 * === 红线（hard rules；违反视同引入安全 bug） ===
 *   1. PTY 字节流（input 或 output）永不入日志，无论截断、preview、还是 hex。
 *      terminal-manager.writeRaw / xterm output 都在此范围。理由：用户在 Claude TUI
 *      粘贴的可能是 API key / token / 密码，PTY 字节流是"用户秘密污染"的。
 *   2. 聊天消息 body 永不整段入日志；仅 { len, preview: text.slice(0, 80) }。
 *   3. 文件内容永不入日志；仅 { path, size }。
 *   4. REDACT_KEYS 白名单外的可疑字段按 "宁愿 redact 不冒险" 处理。
 *
 * === 用法 ===
 *   启动时：  await initLogger(); installFatalHandlers();
 *   模块：    const log = modLogger('terminal'); log.info({ projectId }, 'started');
 *   HTTP:     als.run({ reqId, user }, () => next())  —— middleware 里一行，下游自动带
 *   子进程:   子进程 runId 由调用方生成并塞进 mixin（见 sync / update 模块）
 *   退出:     SIGTERM 走 flushLogger() + server.close() + process.exit(0)
 *             uncaught 异常由 installFatalHandlers 接管，保证 rollStream.flushSync 后才退
 *
 * === 运行期 toggle ===
 *   kill -USR1 $(cat ~/.ccweb/ccweb.pid)   在 info / debug 之间切换级别，无需重启
 */
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.als = void 0;
exports.runWithContext = runWithContext;
exports.initLogger = initLogger;
exports.getLogger = getLogger;
exports.modLogger = modLogger;
exports.installFatalHandlers = installFatalHandlers;
exports.flushLogger = flushLogger;
const pino_1 = __importDefault(require("pino"));
const pino_roll_1 = __importDefault(require("pino-roll"));
const async_hooks_1 = require("async_hooks");
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
const os = __importStar(require("os"));
// ────────────────── 常量 ──────────────────
const LOGS_DIR = path.join(os.homedir(), '.ccweb', 'logs');
const VERSION = (() => {
    try {
        const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json'), 'utf-8'));
        return `v${pkg.version}`;
    }
    catch {
        return 'v?';
    }
})();
const DEFAULT_MAX_MB = parseInt(process.env.CCWEB_LOG_MAX_MB_PER_DAY || '20', 10);
const DEFAULT_RETAIN = parseInt(process.env.CCWEB_LOG_RETAIN_DAYS || '7', 10);
const DEFAULT_LEVEL = process.env.CCWEB_LOG_LEVEL || 'info';
/** Fields whose values are always replaced with '***'. */
const REDACT_KEYS = [
    'password',
    'passwordHash',
    'token',
    'jwtSecret',
    'approvalSecret',
    'authorization',
    'apiKey',
    'SSHPASS',
    'hubToken',
    'publishToken',
    'oauthToken',
    'accessToken',
    'refreshToken',
    '*.password',
    '*.token',
    '*.secret',
    'req.headers.authorization',
    'req.headers.cookie',
];
/**
 * Regex redactors applied to every string value we encounter — AND to nested
 * err.message / err.stack (pino's built-in err serializer unpacks Error into
 * `{type, message, stack}`, which bypasses both REDACT_KEYS and the shallow
 * top-level sweep unless we handle err explicitly).
 *
 * Threshold for hex redaction is 64 chars (SHA-256 / generated 32-byte hex
 * secrets). 40-hex would false-positive on git SHAs and unlucky directory
 * names (reviewer C2).
 *
 * GitHub PAT prefixes cover ghp_/gho_/ghu_/ghs_/ghr_ (legacy) and
 * github_pat_ (fine-grained). skillhub routes user PATs through this
 * logger when ops throw (reviewer N6 — zero-cost defence).
 */
const REGEX_REDACTORS = [
    [/Bearer [A-Za-z0-9._\-]+/g, 'Bearer ***'],
    [/npm_[A-Za-z0-9]{20,}/g, 'npm_***'],
    [/\b(ghp_|gho_|ghu_|ghs_|ghr_|github_pat_)[A-Za-z0-9_]{20,}\b/g, 'gh_***'],
    [/\b[a-f0-9]{64,}\b/gi, 'hex:***'],
];
function redactString(s) {
    let out = s;
    for (const [re, rep] of REGEX_REDACTORS)
        out = out.replace(re, rep);
    return out;
}
/**
 * Recursively redact message/stack along the `err.cause` chain.
 *
 * Reviewer Critical #1: pino's err serializer expands `{cause}` into its own
 * nested `{type, message, stack}` object in the final output; the parent's
 * `err.stack` also contains a "Caused by:" block with the child stack joined.
 * Single-level redact (obj.err.message + obj.err.stack) misses cause.message
 * / cause.stack as separate fields. Depth cap 3 prevents infinite loops on
 * self-referential causes and caps CPU on malicious err graphs.
 */
function redactErrDeep(err, depth = 0) {
    if (!err || typeof err !== 'object' || depth > 3)
        return;
    const e = err;
    if (typeof e.message === 'string')
        e.message = redactString(e.message);
    if (typeof e.stack === 'string')
        e.stack = redactString(e.stack);
    if (e.cause)
        redactErrDeep(e.cause, depth + 1);
}
exports.als = new async_hooks_1.AsyncLocalStorage();
function runWithContext(ctx, fn) {
    return exports.als.run(ctx, fn);
}
// ────────────────── Logger 实例（延迟初始化）──────────────────
let logger = null;
// pino-roll returns a SonicBoom stream; we hold it for flushSync on fatal exit.
let rollStream = null;
function ensureDir() {
    try {
        fs.mkdirSync(LOGS_DIR, { mode: 0o700, recursive: true });
        // chmod even if dir existed (recreated between versions)
        fs.chmodSync(LOGS_DIR, 0o700);
    }
    catch {
        /* best effort */
    }
}
async function initLogger() {
    if (logger)
        return logger;
    ensureDir();
    rollStream = await (0, pino_roll_1.default)({
        file: path.join(LOGS_DIR, 'ccweb'),
        dateFormat: 'yyyy-MM-dd',
        extension: '.log',
        frequency: 'daily',
        size: `${DEFAULT_MAX_MB}m`,
        limit: { count: DEFAULT_RETAIN },
        mkdir: true,
        // sonic-boom passthrough: create log files 0600 (owner-only)
        mode: 0o600,
    });
    logger = (0, pino_1.default)({
        level: DEFAULT_LEVEL,
        base: { ver: VERSION, pid: process.pid },
        timestamp: pino_1.default.stdTimeFunctions.isoTime,
        messageKey: 'msg',
        errorKey: 'err',
        formatters: {
            level: (label) => ({ lvl: label }),
            log: (obj) => {
                // ALS injection: HTTP/WS handlers put reqId/user into ALS;
                // any log event downstream in the same async chain auto-tags.
                const ctx = exports.als.getStore();
                if (ctx) {
                    if (ctx.reqId && !obj.reqId)
                        obj.reqId = ctx.reqId;
                    if (ctx.user && !obj.user)
                        obj.user = ctx.user;
                    if (ctx.projectId && !obj.projectId)
                        obj.projectId = ctx.projectId;
                    if (ctx.wsId && !obj.wsId)
                        obj.wsId = ctx.wsId;
                    if (ctx.runId && !obj.runId)
                        obj.runId = ctx.runId;
                }
                // Shallow regex sweep over string values.
                for (const k of Object.keys(obj)) {
                    const v = obj[k];
                    if (typeof v === 'string')
                        obj[k] = redactString(v);
                }
                // Deep sweep on serialized Error along the `cause` chain.
                // pino's err serializer expands nested err.cause into its own
                // {type, message, stack} sub-object; the parent stack also
                // contains a "Caused by:" block. Single-level redact misses
                // cause.message / cause.stack as separate fields.
                // Reviewer C1 + new: undici (node fetch) errors surface TLS /
                // DNS cause with stack occasionally bearing cert fingerprints /
                // cookie headers / token fragments. Depth 3 caps self-ref loops.
                if (obj.err)
                    redactErrDeep(obj.err);
                return obj;
            },
        },
        redact: {
            paths: REDACT_KEYS,
            censor: '***',
        },
    }, rollStream);
    // Runtime level toggle without restart.
    process.on('SIGUSR1', () => {
        if (!logger)
            return;
        logger.level = logger.level === 'debug' ? 'info' : 'debug';
        logger.info({ mod: 'server', level: logger.level }, 'log level toggled');
    });
    return logger;
}
/** Get the base logger. Caller MUST initLogger() first. */
function getLogger() {
    if (!logger)
        throw new Error('logger not initialized — call initLogger() first');
    return logger;
}
/**
 * Module-scoped child logger.
 *
 * Safe to call at module top level: returns a lazy Proxy that defers the
 * actual `logger.child({ mod })` call until the first `.info/.warn/...`.
 * This lets any module do `const log = modLogger('x')` regardless of
 * import order relative to `initLogger()`.
 *
 * If called after init, child is created eagerly (no Proxy cost).
 *
 * Pre-init calls fall back to a console-shim — bootstrap-phase events
 * still surface via daemon stdout → `~/.ccweb/ccweb.log` (plan §11). Once
 * pino is ready, the Proxy swaps to the real child for the next call.
 */
function modLogger(mod) {
    if (logger)
        return logger.child({ mod });
    // Pre-init fallback: shim mapping pino-style calls to console.
    // Never throws — bootstrap code does not break if it logs before pino.
    const bootstrapShim = {
        fatal: (obj, msg) => console.error(`[boot ${mod}] fatal`, msg ?? '', obj),
        error: (obj, msg) => console.error(`[boot ${mod}] ${msg ?? ''}`, obj),
        warn: (obj, msg) => console.warn(`[boot ${mod}] ${msg ?? ''}`, obj),
        info: (obj, msg) => console.log(`[boot ${mod}] ${msg ?? ''}`, obj),
        debug: () => { },
        trace: () => { },
        child: () => bootstrapShim,
    };
    let cached = null;
    const resolve = () => {
        if (cached)
            return cached;
        if (!logger)
            return bootstrapShim; // still pre-init — route to console
        cached = logger.child({ mod });
        return cached;
    };
    // Proxy every access through resolve(). Bind functions back to the target
    // so `this` stays correct on both shim and real child.
    return new Proxy({}, {
        get(_target, prop) {
            const target = resolve();
            const val = target[prop];
            return typeof val === 'function' ? val.bind(target) : val;
        },
        set(_target, prop, value) {
            const target = resolve();
            target[prop] = value;
            return true;
        },
    });
}
// ────────────────── Fatal handlers ──────────────────
/**
 * Install process-level uncaughtException / unhandledRejection handlers
 * that write the fatal record, then call flushSync on the pino-roll stream
 * (sonic-boom) before exit. Without the flushSync, sonic-boom's async buffer
 * would drop the very stack trace we most need to investigate
 * (reviewer Critical #1).
 *
 * Note: pino v10 removed `pino.final` — the recommended pattern is direct
 * flushSync on the sonic-boom destination (pino-roll returns a SonicBoom).
 */
function installFatalHandlers() {
    const base = logger;
    if (!base)
        throw new Error('logger not initialized');
    const handler = (err, origin) => {
        try {
            base.fatal({ err, origin, mod: 'server' }, 'fatal — process exiting');
        }
        catch {
            /* don't let logging fail stop exit */
        }
        try {
            if (rollStream && typeof rollStream.flushSync === 'function') {
                rollStream.flushSync();
            }
        }
        catch {
            /* flush best-effort */
        }
        process.exit(1);
    };
    process.on('uncaughtException', (err) => handler(err, 'uncaughtException'));
    process.on('unhandledRejection', (reason) => {
        const err = reason instanceof Error ? reason : new Error(String(reason));
        handler(err, 'unhandledRejection');
    });
}
/**
 * Graceful flush — for SIGTERM / planned shutdown. Non-fatal path.
 * Returns a promise that resolves after pino has flushed.
 */
function flushLogger() {
    return new Promise((resolve) => {
        if (!logger) {
            resolve();
            return;
        }
        logger.flush(() => resolve());
    });
}
//# sourceMappingURL=logger.js.map