/**
 * AgentAuth -- Inter-agent authentication via HMAC-signed handoffs.
 *
 * Prevents handoff forgery between agents. Each agent has a derived
 * secret from its name + SOUL content. All handoffs carry HMAC-SHA256
 * signatures that are verified before acceptance.
 *
 * Addresses OWASP ASI07 (Inter-Agent Authentication).
 *
 * @connexum/ai-governance
 */
import * as crypto from 'crypto';
import * as fs from 'fs';
import * as path from 'path';
import { NonceStore } from './nonce-store.js';
export class AgentAuth {
    config;
    /** Cache of agent secrets derived from identity files */
    secretCache = new Map();
    /** Used nonces with timestamps (prevent replay with TTL-based expiry) */
    usedNonces = new Map();
    /** Persistent nonce store (survives process restart) */
    nonceStore = null;
    constructor(config) {
        this.config = {
            agentsDir: config.agentsDir,
            secretSeed: config.secretSeed,
            handoffLogPath: config.handoffLogPath || path.join(config.agentsDir, '..', 'audit', 'handoffs.jsonl'),
            signatureWindowMs: config.signatureWindowMs ?? 300000,
            nonceStoreDir: config.nonceStoreDir || '',
        };
        // Initialize persistent nonce store if directory is provided
        if (this.config.nonceStoreDir) {
            this.nonceStore = new NonceStore({
                storePath: path.join(this.config.nonceStoreDir, 'handoff-nonces.jsonl'),
                ttlMs: this.config.signatureWindowMs * 2,
            });
        }
    }
    /**
     * Create a signed handoff from one agent to another.
     */
    createHandoff(from, to, task, payload) {
        const timestamp = new Date().toISOString();
        const nonce = crypto.randomBytes(16).toString('hex');
        const id = `handoff_${Date.now()}_${nonce.slice(0, 8)}`;
        const message = this.buildSignatureMessage(from, to, task, timestamp, nonce);
        const secret = this.deriveAgentSecret(from);
        const signature = crypto.createHmac('sha256', secret)
            .update(message)
            .digest('hex');
        const handoff = { id, from, to, task, payload, timestamp, signature, nonce };
        this.logHandoff('created', handoff);
        return handoff;
    }
    /**
     * Verify a signed handoff is authentic and hasn't been tampered with.
     */
    verifyHandoff(handoff) {
        // Prune expired nonces before checking (TTL = 2x signature window)
        this.pruneExpiredNonces();
        // Check replay (nonce reuse) -- both in-memory and persistent store
        if (this.usedNonces.has(handoff.nonce) ||
            (this.nonceStore && this.nonceStore.has(handoff.nonce))) {
            this.logHandoff('replay-rejected', handoff);
            return { valid: false, handoff, reason: 'Nonce already used (replay attack)' };
        }
        // Check timestamp window
        const handoffTime = new Date(handoff.timestamp).getTime();
        const now = Date.now();
        if (Math.abs(now - handoffTime) > this.config.signatureWindowMs) {
            this.logHandoff('expired', handoff);
            return { valid: false, handoff, reason: `Handoff expired (${Math.round(Math.abs(now - handoffTime) / 1000)}s old, max ${this.config.signatureWindowMs / 1000}s)` };
        }
        // Verify HMAC signature
        const message = this.buildSignatureMessage(handoff.from, handoff.to, handoff.task, handoff.timestamp, handoff.nonce);
        const secret = this.deriveAgentSecret(handoff.from);
        const expectedSignature = crypto.createHmac('sha256', secret)
            .update(message)
            .digest('hex');
        // F-NEW-CRIT-35: length-check before timingSafeEqual (Node throws
        // ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH on unequal-length buffers).
        // A malformed handoff.signature (wrong length / non-hex) would crash
        // the verifier with an uncaught RangeError. Treat length mismatch as
        // signature-invalid (fail-closed).
        const sigBuf = Buffer.from(handoff.signature, 'hex');
        const expBuf = Buffer.from(expectedSignature, 'hex');
        if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) {
            this.logHandoff('signature-invalid', handoff);
            return { valid: false, handoff, reason: 'HMAC signature verification failed' };
        }
        // Mark nonce as used with timestamp for TTL expiry
        this.usedNonces.set(handoff.nonce, Date.now());
        // Persist to disk for cross-restart replay protection
        if (this.nonceStore) {
            this.nonceStore.add(handoff.nonce);
        }
        this.logHandoff('verified', handoff);
        return { valid: true, handoff };
    }
    /**
     * Accept a handoff: verify + mark as accepted.
     */
    acceptHandoff(handoff, acceptingAgent) {
        // Verify the handoff is addressed to this agent
        if (handoff.to !== acceptingAgent) {
            return {
                valid: false,
                handoff,
                reason: `Handoff addressed to '${handoff.to}', not '${acceptingAgent}'`,
            };
        }
        const verification = this.verifyHandoff(handoff);
        if (verification.valid) {
            this.logHandoff('accepted', handoff, { acceptedBy: acceptingAgent });
        }
        return verification;
    }
    /**
     * Get the agent secret for signing. Derives using HKDF from agent identity + seed.
     * Cached for performance with file modification tracking.
     */
    deriveAgentSecret(agentName) {
        // Check cache with file modification invalidation
        const cached = this.secretCache.get(agentName);
        const cacheKey = `${agentName}:mtime`;
        const cachedMtime = this.secretCache.get(cacheKey);
        // Try to read agent's SOUL/identity content for additional entropy
        let identityContent = '';
        let fileMtime = '';
        const possibleFiles = [
            path.join(this.config.agentsDir, `${agentName}.md`),
            path.join(this.config.agentsDir, `${agentName}-agent.md`),
        ];
        for (const file of possibleFiles) {
            if (fs.existsSync(file)) {
                try {
                    const stat = fs.statSync(file);
                    fileMtime = stat.mtimeMs.toString();
                    const content = fs.readFileSync(file, 'utf-8');
                    // Extract SOUL section if present
                    const soulMatch = content.match(/## SOUL[^\n]*\n([\s\S]*?)(?=\n## |\n---|\Z)/);
                    identityContent = soulMatch ? soulMatch[1] : content.slice(0, 200);
                }
                catch {
                    // Fall through to seed-only derivation
                }
                break;
            }
        }
        // Return cached secret only if file hasn't changed
        if (cached && cachedMtime === fileMtime)
            return cached;
        // HKDF-like derivation: extract then expand
        // Step 1: Extract -- PRK from seed + identity
        const salt = crypto.createHash('sha256')
            .update(`connexum-agent-auth-salt:${agentName}`)
            .digest();
        const ikm = `${this.config.secretSeed}:${agentName}:${identityContent}`;
        const prk = crypto.createHmac('sha256', salt)
            .update(ikm)
            .digest();
        // Step 2: Expand -- derive final key
        const info = Buffer.from(`connexum-agent-secret-v1:${agentName}`);
        const secret = crypto.createHmac('sha256', prk)
            .update(Buffer.concat([info, Buffer.from([1])]))
            .digest('hex');
        this.secretCache.set(agentName, secret);
        this.secretCache.set(cacheKey, fileMtime);
        return secret;
    }
    /**
     * Generate a shared verification token that two agents can use
     * to establish a trusted channel.
     */
    createChannelToken(agent1, agent2) {
        const secret1 = this.deriveAgentSecret(agent1);
        const secret2 = this.deriveAgentSecret(agent2);
        // Deterministic: always combine in sorted order
        const [a, b] = [secret1, secret2].sort();
        return crypto.createHash('sha256')
            .update(`channel:${a}:${b}`)
            .digest('hex');
    }
    /**
     * List recent handoffs from the log.
     */
    getRecentHandoffs(limit = 50) {
        const logPath = this.config.handoffLogPath;
        if (!fs.existsSync(logPath))
            return [];
        const lines = fs.readFileSync(logPath, 'utf-8')
            .split('\n')
            .filter(Boolean)
            .slice(-limit);
        return lines.map(line => {
            try {
                return JSON.parse(line);
            }
            catch {
                return { raw: line };
            }
        });
    }
    /**
     * Clear the nonce cache (for testing or maintenance).
     */
    clearNonces() {
        this.usedNonces.clear();
    }
    /**
     * Prune nonces older than 2x the signature window.
     * Prevents unbounded memory growth while maintaining replay protection
     * for all nonces within the validity window.
     */
    pruneExpiredNonces() {
        const ttl = this.config.signatureWindowMs * 2;
        const now = Date.now();
        for (const [nonce, timestamp] of this.usedNonces) {
            if (now - timestamp > ttl) {
                this.usedNonces.delete(nonce);
            }
        }
    }
    /**
     * Clear the secret cache (if agent files change).
     */
    clearSecretCache() {
        this.secretCache.clear();
    }
    // --- Private helpers ---
    buildSignatureMessage(from, to, task, timestamp, nonce) {
        // Use JSON canonical form to avoid delimiter collision (pipe chars in field values)
        return JSON.stringify([from, to, task, timestamp, nonce]);
    }
    logHandoff(event, handoff, extra) {
        const logDir = path.dirname(this.config.handoffLogPath);
        if (!fs.existsSync(logDir)) {
            fs.mkdirSync(logDir, { recursive: true });
        }
        const entry = {
            timestamp: new Date().toISOString(),
            event: `handoff:${event}`,
            handoffId: handoff.id,
            from: handoff.from,
            to: handoff.to,
            task: handoff.task,
            ...extra,
        };
        fs.appendFileSync(this.config.handoffLogPath, JSON.stringify(entry) + '\n');
    }
}
//# sourceMappingURL=agent-auth.js.map