/**
 * src/security/hardening.ts
 *
 * Multi-layer security hardening for v1.1. The "make legal liability go away"
 * pass. Each control here is independently valuable; together they form the
 * defense surface that lets us sign enterprise contracts.
 *
 * Controls:
 *   1. Prompt injection scanner — detect known jailbreak patterns at inbound
 *   2. Output safety filter — redact secrets in outbound replies
 *   3. Per-contact rate limiter — prevent API abuse
 *   4. Code integrity check (boot-time) — flag dist/ tampering
 *
 * Hash-chain journal lives in journal.ts (separate file because it touches
 * the existing record() entry point).
 */
import { createHash } from 'node:crypto';
import { createChildLogger } from '../utils/logger.js';
const log = createChildLogger('security:hardening');
// ────────────────────────────────────────────────────────────────────────
// 1. Prompt Injection Scanner
// ────────────────────────────────────────────────────────────────────────
//
// These patterns catch the most common jailbreak vectors in inbound
// messages. We don't try to be exhaustive — that's an arms race. We catch
// the LOW-EFFORT 95% so casual attackers fail and skilled attackers have
// to invest real effort (and that effort generates anomalous patterns that
// the rate limiter catches).
//
// On match:
//   • Block delivery to the brain
//   • Journal kind=security_block with full context
//   • Return a politely opaque message ("ما قدرت أتعامل مع طلبك")
//   • Burn-down the sender's rate-limit budget faster
const INJECTION_PATTERNS = [
    // Classic prompt injection
    { name: 'ignore-previous', re: /\b(?:ignore|disregard|forget)\s+(?:all\s+)?(?:previous|prior|above|earlier)\s+(?:instructions?|prompts?|rules?|directives?)\b/i, severity: 'high' },
    { name: 'reveal-system-prompt', re: /\b(?:reveal|show|print|output|dump|repeat|tell me)\s+(?:your|the)\s+(?:system\s+)?(?:prompt|instructions|rules|directives|context)\b/i, severity: 'high' },
    { name: 'role-override', re: /\byou\s+(?:are|will\s+be)\s+now\s+(?:a\s+)?(?:different|new|jailbroken|unrestricted|developer\s+mode|admin)\b/i, severity: 'high' },
    { name: 'dan-jailbreak', re: /\b(?:DAN|do\s+anything\s+now|jailbreak\s+mode|developer\s+mode\s+enabled)\b/i, severity: 'high' },
    { name: 'pretend-instructions', re: /\b(?:pretend|imagine|act\s+as|roleplay\s+as)\s+(?:you\s+(?:are|have))\s+no\s+(?:rules|restrictions|limits|filters)\b/i, severity: 'high' },
    // Arabic equivalents
    { name: 'ar-ignore-previous', re: /(?:تجاهل|انس)\s+(?:جميع|كل)\s+(?:التعليمات|التوجيهات|القواعد)\s+(?:السابقة|أعلاه)/i, severity: 'high' },
    { name: 'ar-reveal-prompt', re: /(?:اعرض|اطبع|أظهر|قل لي)\s+(?:برومبت|تعليمات|قواعد)\s+(?:النظام|المؤسس)/i, severity: 'high' },
    { name: 'ar-role-override', re: /أنت\s+الآن\s+(?:مساعد\s+)?(?:بدون\s+قيود|مختلف|جديد|حر|بدون\s+حدود)/i, severity: 'high' },
    // Encoding-based smuggling
    { name: 'base64-injection', re: /(?:execute|run|eval|decode\s+and\s+run)[^.]{0,40}base64/i, severity: 'high' },
    { name: 'unicode-tag-smuggle', re: /[\u{E0020}-\u{E007E}]{6,}/u, severity: 'high' }, // Tag-block invisible chars
    // System-prompt-leak attempts
    { name: 'leak-soul', re: /\b(?:print|show|reveal|dump)\s+(?:your\s+)?soul\.md\b/i, severity: 'medium' },
    { name: 'leak-muassis-md', re: /\b(?:print|show|reveal|dump|read)\s+(?:your\s+)?(?:MUASSIS|AGENTS|MEMORY)\.md\b/i, severity: 'medium' },
    // Attempts to escalate to the bundled NIM key
    { name: 'extract-api-key', re: /\b(?:show|print|reveal|export|dump|cat|leak)\s+(?:your\s+|the\s+)?(?:nvidia|api|nim|bundled)?\s*(?:key|token|secret|credential)\b/i, severity: 'high' },
];
export function scanForPromptInjection(input) {
    const matches = [];
    if (!input)
        return { clean: true, matches: [], highest: null };
    for (const p of INJECTION_PATTERNS) {
        const m = input.match(p.re);
        if (m) {
            matches.push({
                name: p.name,
                severity: p.severity,
                preview: m[0].slice(0, 100),
            });
        }
    }
    const highest = matches.length === 0
        ? null
        : matches.some(m => m.severity === 'high') ? 'high'
            : matches.some(m => m.severity === 'medium') ? 'medium'
                : 'low';
    return { clean: matches.length === 0, matches, highest };
}
// ────────────────────────────────────────────────────────────────────────
// 2. Output Safety Filter
// ────────────────────────────────────────────────────────────────────────
//
// Redact common secret patterns that might appear in agent output. This is
// a last-line defense — the brain shouldn't be emitting secrets in the
// first place, but if it does (model error, prompt injection, replay of
// stored content), we don't want them on the wire.
const SECRET_PATTERNS = [
    // Common API key formats (broad, low false-positive)
    { name: 'nvidia-key', re: /nvapi-[A-Za-z0-9_-]{60,90}/g, replacement: 'nvapi-****REDACTED****' },
    { name: 'openai-key', re: /sk-[A-Za-z0-9]{20,80}/g, replacement: 'sk-****REDACTED****' },
    { name: 'anthropic-key', re: /sk-ant-[A-Za-z0-9_-]{40,120}/g, replacement: 'sk-ant-****REDACTED****' },
    { name: 'google-key', re: /AIza[A-Za-z0-9_-]{35}/g, replacement: 'AIza****REDACTED****' },
    { name: 'aws-access', re: /AKIA[0-9A-Z]{16}/g, replacement: 'AKIA****REDACTED****' },
    { name: 'github-pat', re: /ghp_[A-Za-z0-9]{36}/g, replacement: 'ghp_****REDACTED****' },
    { name: 'github-fine', re: /github_pat_[A-Za-z0-9_]{82}/g, replacement: 'github_pat_****REDACTED****' },
    { name: 'stripe-key', re: /(?:sk|pk|rk)_(?:test|live)_[A-Za-z0-9]{20,80}/g, replacement: '****REDACTED-STRIPE****' },
    { name: 'jwt', re: /eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, replacement: 'eyJ****REDACTED-JWT****' },
    { name: 'bearer', re: /Bearer\s+[A-Za-z0-9_\-.]{20,200}/gi, replacement: 'Bearer ****REDACTED****' },
    // Saudi-specific
    { name: 'saudi-iban', re: /\bSA\d{22}\b/g, replacement: 'SA****REDACTED****' },
    // Credit cards (Luhn would be more accurate, but pattern catches obvious leaks)
    { name: 'credit-card', re: /\b(?:\d{4}[\s-]?){3}\d{4}\b/g, replacement: '****-****-****-****' },
    // Owner.token (the device auth credential)
    { name: 'muassis-token', re: /\b[a-f0-9]{64}\b/g, replacement: '****REDACTED-TOKEN****' },
];
export function filterOutboundSecrets(text) {
    if (!text)
        return { filtered: text, redactions: [] };
    const counts = new Map();
    let out = text;
    for (const p of SECRET_PATTERNS) {
        const before = out;
        out = out.replace(p.re, p.replacement);
        if (before !== out) {
            const matchCount = (before.match(p.re) ?? []).length;
            counts.set(p.name, (counts.get(p.name) ?? 0) + matchCount);
        }
    }
    return {
        filtered: out,
        redactions: [...counts.entries()].map(([name, count]) => ({ name, count })),
    };
}
const buckets = new Map();
// Generous limits — these are an ABUSE detector, not throttling. A genuine
// customer in active troubleshooting can easily fire 30-50 messages in a
// row; we don't want to crash that flow. Tuned so:
//   • Soft warn (escalate to owner, NEVER block): 100 msg / 5 min — only
//     unusually chatty senders trigger this. Owner sees a notice.
//   • Hard block (refuse to dispatch to brain): 600 msg / 5 min — ~2/sec
//     sustained. That's a bot, not a person.
// Owner can disable entirely via env: MUASSIS_RATE_LIMIT=0
const RATE_WINDOW_MS = 5 * 60 * 1000;
const RATE_SOFT_LIMIT = 100;
const RATE_HARD_LIMIT = 600;
export function checkContactRate(tenantId, identifier) {
    // Bypass entirely if the owner disabled rate-limiting
    if (process.env.MUASSIS_RATE_LIMIT === '0' || process.env.MUASSIS_RATE_LIMIT === 'off') {
        return { allowed: true, countInWindow: 0 };
    }
    const key = `${tenantId}:${identifier}`;
    const now = Date.now();
    let b = buckets.get(key);
    if (!b) {
        b = { timestamps: [], softWarnedAt: null, hardBlockedAt: null };
        buckets.set(key, b);
    }
    // Drop old timestamps outside the window
    b.timestamps = b.timestamps.filter(t => now - t < RATE_WINDOW_MS);
    b.timestamps.push(now);
    const count = b.timestamps.length;
    // Hard block only at truly unreasonable rates (genuine bot/abuse patterns)
    if (count > RATE_HARD_LIMIT) {
        if (!b.hardBlockedAt)
            b.hardBlockedAt = now;
        return { allowed: false, reason: 'hard-block', countInWindow: count };
    }
    // Soft warn = ALLOW request through, but flag once per window so the owner
    // sees the unusual pattern. Never blocks a real customer.
    if (count > RATE_SOFT_LIMIT && !b.softWarnedAt) {
        b.softWarnedAt = now;
        return { allowed: true, reason: 'soft-warn', countInWindow: count };
    }
    return { allowed: true, countInWindow: count };
}
export function clearContactRate(tenantId, identifier) {
    buckets.delete(`${tenantId}:${identifier}`);
}
export async function verifyCodeIntegrity() {
    const result = { ok: true, checked: 0, tampered: [], missing: [] };
    try {
        const { existsSync, readFileSync } = await import('node:fs');
        const { join, dirname, resolve } = await import('node:path');
        const { fileURLToPath } = await import('node:url');
        const distDir = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
        const manifestPath = join(distDir, 'integrity.manifest.json');
        if (!existsSync(manifestPath)) {
            log.debug('No integrity.manifest.json found — skipping verify (dev / unbuilt install)');
            return result;
        }
        const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
        for (const [relPath, expectedHash] of Object.entries(manifest)) {
            const fullPath = join(distDir, relPath);
            if (!existsSync(fullPath)) {
                result.missing.push(relPath);
                continue;
            }
            const actual = createHash('sha256').update(readFileSync(fullPath)).digest('hex');
            result.checked++;
            if (actual !== expectedHash) {
                result.tampered.push(relPath);
            }
        }
        result.ok = result.tampered.length === 0 && result.missing.length === 0;
        if (!result.ok) {
            log.warn({ tampered: result.tampered, missing: result.missing }, '⚠️ Code integrity check failed');
        }
        else if (result.checked > 0) {
            log.info({ checked: result.checked }, 'Code integrity verified');
        }
    }
    catch (err) {
        log.warn({ err: err.message }, 'Code integrity verify threw');
    }
    return result;
}
//# sourceMappingURL=hardening.js.map