/**
 * NonceStore -- Persistent, TTL-based nonce tracking for replay protection.
 *
 * Survives process restarts by persisting nonces to disk.
 * Used by AgentAuth to prevent handoff replay attacks across sessions.
 *
 * Format: JSONL file with { nonce, timestamp } entries.
 * Prunes expired entries on load and periodically during use.
 *
 * @connexum/ai-governance
 */
import * as fs from 'fs';
import * as path from 'path';
export class NonceStore {
    config;
    nonces = new Map();
    dirty = false;
    constructor(config) {
        this.config = {
            ttlMs: 600000,
            maxEntries: 10000,
            ...config,
        };
        const dir = path.dirname(config.storePath);
        if (!fs.existsSync(dir)) {
            fs.mkdirSync(dir, { recursive: true });
        }
        this.loadFromDisk();
    }
    /**
     * Check if a nonce has been seen. Returns true if it's a replay.
     */
    has(nonce) {
        this.pruneIfNeeded();
        return this.nonces.has(nonce);
    }
    /**
     * Record a nonce as used. Returns false if already used (replay).
     */
    add(nonce) {
        this.pruneIfNeeded();
        if (this.nonces.has(nonce)) {
            return false; // Replay
        }
        this.nonces.set(nonce, Date.now());
        this.dirty = true;
        this.flush();
        return true;
    }
    /**
     * Get count of active (non-expired) nonces.
     */
    size() {
        this.pruneIfNeeded();
        return this.nonces.size;
    }
    /**
     * Force flush to disk.
     */
    flush() {
        if (!this.dirty && fs.existsSync(this.config.storePath))
            return;
        try {
            const entries = [];
            for (const [nonce, ts] of this.nonces) {
                entries.push({ nonce, ts });
            }
            fs.writeFileSync(this.config.storePath, entries.map(e => JSON.stringify(e)).join('\n') + '\n');
            this.dirty = false;
        }
        catch (err) {
            process.stderr.write(`[NONCE STORE] Failed to persist: ${err.message}\n`);
        }
    }
    /**
     * Clear all nonces (for testing).
     */
    clear() {
        this.nonces.clear();
        this.dirty = true;
        this.flush();
    }
    // --- Private ---
    pruneIfNeeded() {
        const now = Date.now();
        let pruned = false;
        for (const [nonce, ts] of this.nonces) {
            if (now - ts > this.config.ttlMs) {
                this.nonces.delete(nonce);
                pruned = true;
            }
        }
        // Force prune if over max
        if (this.nonces.size > this.config.maxEntries) {
            const sorted = Array.from(this.nonces.entries())
                .sort((a, b) => a[1] - b[1]);
            const toRemove = sorted.slice(0, sorted.length - this.config.maxEntries);
            for (const [nonce] of toRemove) {
                this.nonces.delete(nonce);
            }
            pruned = true;
        }
        if (pruned) {
            this.dirty = true;
            this.flush();
        }
    }
    loadFromDisk() {
        if (!fs.existsSync(this.config.storePath))
            return;
        try {
            const content = fs.readFileSync(this.config.storePath, 'utf-8');
            const now = Date.now();
            const lines = content.trim().split('\n').filter(Boolean);
            for (const line of lines) {
                try {
                    const entry = JSON.parse(line);
                    // Only load non-expired nonces
                    if (now - entry.ts <= this.config.ttlMs) {
                        this.nonces.set(entry.nonce, entry.ts);
                    }
                }
                catch {
                    // Skip malformed lines
                }
            }
        }
        catch (err) {
            process.stderr.write(`[NONCE STORE] Failed to load: ${err.message}\n`);
        }
    }
}
//# sourceMappingURL=nonce-store.js.map