/**
 * Shared utilities for IDE adapters.
 *
 * User-identity resolution, canonical audit-event builders, session-handle
 * generation. These live exactly once here; every IDE adapter imports what
 * it needs rather than reimplementing.
 *
 * Spec: P3-IDE-01 Section 3.4, Section 3.2
 *
 * @connexum/ai-governance
 */
import * as fs from 'node:fs';
import * as crypto from 'node:crypto';
import { IdeAttributionError } from './index.js';
// ---------------------------------------------------------------------------
// Session handle generation
// ---------------------------------------------------------------------------
/**
 * Generate a collision-resistant session handle for an IDE adapter session.
 * Format: <ideId>-<8-hex-random>-<unix-ms>
 */
export function generateSessionHandle(ideId) {
    const rand = crypto.randomBytes(4).toString('hex');
    const ts = Date.now();
    return {
        sessionId: `${ideId}-${rand}-${ts}`,
        openedAt: new Date().toISOString(),
    };
}
// ---------------------------------------------------------------------------
// User identity resolution helpers
// ---------------------------------------------------------------------------
/**
 * Attempt to read user identity from the `.claude/session.json` file.
 * Returns the userId string if found, or null if the file is absent or
 * does not contain a resolvable userId field.
 *
 * This is a best-effort read. Throws are caught and converted to null so
 * the caller decides how to fail.
 */
export function readClaudeSessionFile(sessionFilePath = '.claude/session.json') {
    try {
        const raw = fs.readFileSync(sessionFilePath, 'utf-8');
        const parsed = JSON.parse(raw);
        if (typeof parsed === 'object' &&
            parsed !== null &&
            'userId' in parsed &&
            typeof parsed['userId'] === 'string') {
            const uid = parsed['userId'];
            return uid.trim() || null;
        }
        return null;
    }
    catch {
        return null;
    }
}
/**
 * Resolve a `UserIdentity` for the Claude Code adapter.
 *
 * Resolution order:
 *   1. `CLAUDE_SESSION_USER` environment variable
 *   2. `.claude/session.json` userId field
 *   3. Throws `IdeAttributionError` (fail-closed per Thomas philosophy)
 *
 * Anonymous sessions are permitted ONLY when `allowAnonymousIdeSessions`
 * is explicitly true in the adapter config. Otherwise this always throws
 * on failure to resolve.
 *
 * @param sessionFilePath - Override path to session.json (for tests)
 * @param allowAnonymous - Must be explicitly true to allow anonymous sessions
 */
export function resolveClaudeCodeUserIdentity(sessionFilePath = '.claude/session.json', allowAnonymous = false) {
    // 1. env var
    const envUser = process.env['CLAUDE_SESSION_USER']?.trim();
    if (envUser) {
        return {
            userId: envUser,
            attributionMethod: 'session-env',
        };
    }
    // 2. session.json
    const fileUser = readClaudeSessionFile(sessionFilePath);
    if (fileUser) {
        return {
            userId: fileUser,
            attributionMethod: 'session-file',
        };
    }
    // 3. Anonymous check
    if (allowAnonymous) {
        return {
            userId: 'anonymous',
            attributionMethod: 'session-env',
        };
    }
    // 4. Fail closed
    throw new IdeAttributionError({
        message: 'ClaudeCodeIdeAdapter: cannot resolve user identity. Set the ' +
            'CLAUDE_SESSION_USER environment variable, or provide a ' +
            '.claude/session.json file with a userId field. To permit anonymous ' +
            'sessions, set allowAnonymousIdeSessions: true in adapter config.',
        ideId: 'claude-code',
        adapterId: 'claude-code',
    });
}
/**
 * Build a canonical IDE audit event with all required fields.
 * Severity is inferred from the event type per the taxonomy table.
 */
export function buildIdeAuditEvent(type, opts) {
    return {
        type,
        severity: inferSeverity(type),
        ideId: opts.ideId,
        adapterId: opts.adapterId,
        sessionId: opts.sessionId,
        initiatingUserId: opts.initiatingUserId,
        tool: opts.tool,
        detail: opts.detail,
        timestamp: new Date().toISOString(),
        data: opts.data,
    };
}
// ---------------------------------------------------------------------------
// Severity inference from canonical event type
// ---------------------------------------------------------------------------
const SEVERITY_MAP = {
    'ide:session-open': 'INFO',
    'ide:session-close': 'INFO',
    'ide:tool-invocation-approved': 'INFO',
    'ide:tool-invocation-denied': 'WARN',
    'ide:tool-invocation-require-approval': 'INFO',
    'ide:user-identity-resolved': 'DEBUG',
    'ide:user-identity-unresolved': 'CRITICAL',
    'ide:attribution-ambiguous': 'WARN',
    'ide:pr-check-created': 'INFO',
    'ide:pr-check-passed': 'INFO',
    'ide:pr-check-failed': 'WARN',
    'ide:pr-check-neutral': 'WARN',
    'ide:api-version-mismatch': 'WARN',
    'ide:circuit-breaker-tripped': 'CRITICAL',
};
function inferSeverity(type) {
    return SEVERITY_MAP[type] ?? 'INFO';
}
// ---------------------------------------------------------------------------
// Destructive-command and sensitive-path detection (shared across adapters)
//
// Both CursorIdeAdapter and ContinueDevIdeAdapter use these patterns.
// Single source of truth here; importing adapters do not re-declare.
// ---------------------------------------------------------------------------
export const SHARED_DESTRUCTIVE_PATTERNS = [
    /\brm\s+(-[rR]f?|--recursive)\b/,
    /\bgit\s+push\s+--force\b/,
    /\bgit\s+reset\s+--hard\b/,
    /\bgit\s+clean\s+-f/,
    /\bdrop\s+(table|database)\b/i,
    /\btruncate\s+table\b/i,
    /\bnpm\s+publish\b/,
    /\bcurl\b.*\|\s*(bash|sh)\b/,
    /\bdd\s+if=/,
    /\bchmod\s+777\b/,
    /\bsudo\s+rm\b/,
];
export const SHARED_SENSITIVE_PATH_PATTERNS = [
    /^\/etc\//,
    /^\/root\//,
    /(^|\/)\.env$/, // .env at any path depth
    /\.env\./, // .env.local, .env.production, etc.
    /^\/proc\//,
    /^\/sys\//,
    /secrets?\//i,
    /private_?key/i,
    /\.pem$/i,
    /\.key$/i,
];
/**
 * C6 fix: split a shell command string on the common separator tokens so
 * that each sub-command is scanned independently for destructive patterns.
 *
 * Without this, a chained command like "echo safe; rm -rf /" evades
 * detection because the full string does not match any single pattern --
 * only the sub-command after the semicolon does.
 *
 * Separators covered:
 *   ;    sequential execution
 *   &&   conditional-AND
 *   ||   conditional-OR
 *   $(   command substitution (inner command runs first)
 *   `    backtick command substitution
 */
export function splitOnShellSeparators(command) {
    return command
        .split(/;|&&|\|\||\$\(|`/)
        .map((s) => s.replace(/\)\s*$/, '').trim())
        .filter((s) => s.length > 0);
}
/**
 * Returns true if the command string -- or any of its shell-separated
 * sub-commands -- matches any destructive pattern.
 *
 * Used by CursorIdeAdapter (filesystem-sidecar mode) and ContinueDevIdeAdapter.
 */
export function isDestructiveCommand(command, extraPatterns = []) {
    const all = [...SHARED_DESTRUCTIVE_PATTERNS, ...extraPatterns];
    // C6 fix: scan each shell sub-command individually
    const segments = splitOnShellSeparators(command);
    for (const segment of segments) {
        for (const pattern of all) {
            if (pattern.test(segment)) {
                return { matched: true, pattern, segment };
            }
        }
    }
    return { matched: false };
}
/**
 * Returns true if the file path matches any sensitive-path pattern.
 * Used by CursorIdeAdapter and ContinueDevIdeAdapter.
 */
export function isSensitivePath(filePath, extraPatterns = []) {
    const all = [...SHARED_SENSITIVE_PATH_PATTERNS, ...extraPatterns];
    for (const pattern of all) {
        if (pattern.test(filePath)) {
            return { matched: true, pattern };
        }
    }
    return { matched: false };
}
const SECRET_PATTERNS = [
    { name: 'AWS access key', re: /AKIA[0-9A-Z]{16}/ },
    { name: 'AWS secret key', re: /aws_secret_access_key\s*=\s*\S{20,}/i },
    { name: 'GitHub token', re: /ghp_[0-9A-Za-z]{36}|github_pat_[0-9A-Za-z_]{82}/ },
    { name: 'Anthropic API key', re: /sk-ant-[0-9A-Za-z\-_]{20,}/ },
    { name: 'OpenAI API key', re: /sk-[0-9A-Za-z]{20,}/ },
    { name: 'Private key header', re: /-----BEGIN (RSA|EC|OPENSSH) PRIVATE KEY-----/ },
    { name: 'Generic secret env var', re: /(SECRET|PASSWORD|PASSWD|API_KEY|PRIVATE_KEY)\s*=\s*["']?\S{8,}["']?/i },
    { name: 'Bearer token', re: /Authorization:\s*Bearer\s+[0-9A-Za-z\-._~+/]{20,}/i },
    { name: 'Database URL with creds', re: /(postgres|mysql|mongodb|redis):\/\/[^:]+:[^@]+@/ },
];
/**
 * Scan a unified diff string for added lines containing secret patterns.
 * Returns an array of findings. Empty array means no findings.
 */
export function scanDiffForSecrets(diff) {
    const findings = [];
    const lines = diff.split('\n');
    lines.forEach((line, idx) => {
        // Only scan added lines; skip diff header lines ('+++')
        if (!line.startsWith('+') || line.startsWith('+++'))
            return;
        const content = line.slice(1); // strip the leading '+'
        for (const { name, re } of SECRET_PATTERNS) {
            if (re.test(content)) {
                findings.push({
                    line: idx + 1,
                    pattern: name,
                    excerpt: content.slice(0, 80).replace(/\n/g, ' '),
                });
                break; // one finding per line is enough
            }
        }
    });
    return findings;
}
//# sourceMappingURL=shared.js.map