/**
 * ReceiptQueue — tamper-evident in-memory queue for TSA receipt retries.
 *
 * CH1-r: Every entry is HMAC-SHA-256 authenticated. process() rejects entries
 * with invalid HMACs before submitting to the TSA — no silent data corruption.
 *
 * Backoff schedule: DEFAULT_BACKOFF_MS defines 5 exponential windows.
 * process() skips entries whose next retry time has not been reached yet
 * (based on queuedAt + DEFAULT_BACKOFF_MS[attempts - 1]).
 *
 * KeyProvider isolation: the key is fetched from the provider at enqueue and
 * process time — never stored in the entry itself.
 *
 * @module proof/receipt-queue
 */
import { canonicalize } from "../trace/canonical.js";
import { hmacSha256 } from "./sha256.js";
/**
 * Default exponential backoff schedule (5 levels).
 * Level 0 = 30s, 1 = 5min, 2 = 1h, 3 = 6h, 4 = 24h.
 */
export const DEFAULT_BACKOFF_MS = [
    30_000,
    5 * 60_000,
    60 * 60_000,
    6 * 60 * 60_000,
    24 * 60 * 60_000,
];
// ─── Errors ───────────────────────────────────────────────────────────────────
/**
 * Thrown internally when a queue entry HMAC fails verification.
 * The error is caught by process() and counted in the `rejected` bucket.
 */
export class HmacInvalidError extends Error {
    constructor(runId) {
        super(`HmacInvalidError: HMAC verification failed for entry runId="${runId}". ` +
            "The entry may have been tampered with. Entry rejected without TSA call.");
        this.name = "HmacInvalidError";
    }
}
// ─── HMAC helpers ─────────────────────────────────────────────────────────────
async function computeEntryHmac(entry, keyProvider, keyId) {
    const key = await keyProvider.getKey(keyId);
    // Canonical representation excludes the hmac field itself.
    const canonical = canonicalize({
        attempts: entry.attempts,
        queuedAt: entry.queuedAt,
        rootHash: entry.rootHash,
        runId: entry.runId,
    });
    return hmacSha256(key, canonical);
}
// ─── InMemoryReceiptQueue ─────────────────────────────────────────────────────
/**
 * In-memory ReceiptQueue implementation.
 *
 * Not durable across restarts — for production persistence, implement
 * ReceiptQueue against a PostgreSQL table (see sprint-562 B2).
 */
export class InMemoryReceiptQueue {
    _entries = [];
    async enqueue(entry, keyProvider, keyId) {
        // Emit colocation warning if EnvKeyProvider + TRACE_DB_URL detected.
        if (keyProvider.hasColocationRisk()) {
            process.stderr.write(`[WARN] InMemoryReceiptQueue.enqueue: KeyProvider reports colocation risk ` +
                `(key and TRACE_DB_URL on same host). ` +
                `For production audit compliance, migrate to ExternalKMSKeyProvider.\n`);
        }
        const withAttempts = { ...entry, attempts: 0 };
        const hmac = await computeEntryHmac(withAttempts, keyProvider, keyId);
        this._entries.push({ ...withAttempts, hmac });
    }
    async pending() {
        return [...this._entries];
    }
    async process(adapter, keyProvider, keyId, opts) {
        const backoffMs = opts?.backoffMs ?? DEFAULT_BACKOFF_MS;
        const batchSize = opts?.batchSize ?? this._entries.length;
        let processed = 0;
        let failed = 0;
        let rejected = 0;
        const now = Date.now();
        const toRemove = [];
        let count = 0;
        for (let i = 0; i < this._entries.length && count < batchSize; i++) {
            const entry = this._entries[i];
            // CH1-r: recompute HMAC and compare before calling adapter.
            const expectedHmac = await computeEntryHmac({ attempts: entry.attempts, queuedAt: entry.queuedAt, rootHash: entry.rootHash, runId: entry.runId }, keyProvider, keyId);
            if (entry.hmac !== expectedHmac) {
                rejected++;
                toRemove.push(i); // Remove tampered entries immediately.
                count++;
                continue;
            }
            // Backoff check: skip if retry window hasn't elapsed.
            if (entry.attempts > 0) {
                const backoffIdx = Math.min(entry.attempts - 1, backoffMs.length - 1);
                const nextRetryAt = entry.queuedAt + backoffMs[backoffIdx];
                if (now < nextRetryAt) {
                    // Not due yet — skip but do not count.
                    continue;
                }
            }
            count++;
            try {
                // Try to request the TSA receipt.
                const _receipt = await adapter.request(entry.rootHash);
                void _receipt;
                // Success — mark for removal.
                toRemove.push(i);
                processed++;
            }
            catch {
                // Failure — increment attempts and recompute HMAC for updated entry.
                entry.attempts += 1;
                const newHmac = await computeEntryHmac({ attempts: entry.attempts, queuedAt: entry.queuedAt, rootHash: entry.rootHash, runId: entry.runId }, keyProvider, keyId);
                entry.hmac = newHmac;
                failed++;
            }
        }
        // Remove processed/rejected entries (reverse order to keep indices stable).
        for (const idx of toRemove.slice().sort((a, b) => b - a)) {
            this._entries.splice(idx, 1);
        }
        return { processed, failed, rejected };
    }
    async size() {
        return this._entries.length;
    }
}
//# sourceMappingURL=receipt-queue.js.map