import { createHmac, randomBytes } from "node:crypto";
/**
 * Opaque filter token for the `memory-find-candidates` → `memory-delete` handshake.
 *
 * The admin agent calls `memory-find-candidates(filter)` to produce a list of
 * bulk-delete candidates, and must then pass the returned `filterToken` to
 * `memory-delete` so the server can re-verify that the target elementId still
 * matches the original predicate at trash time. The token closes the 2026-04-22
 * class of failure where the agent authored a bad selection cypher and then
 * dispatched a subagent with the hardcoded id list framed as "owner approved,
 * do not reverify."
 *
 * Format: `base64url(payload).base64url(hmac)` where `payload` is a JSON
 * envelope carrying the filter name, agentType, accountId, issuedAt, and a
 * random nonce. `hmac` is HMAC-SHA256 signed with a process-startup random
 * secret — the secret lives only in memory and dies with the MCP server. A
 * restart invalidates every in-flight token, which harmlessly forces the
 * agent to re-run `memory-find-candidates` rather than proceed on a stale
 * assumption; that is the correct failure mode.
 *
 * TTL is enforced at verify time (default 5 min). Past TTL, verify returns
 * `outcome=expired`; the caller surfaces that verbatim to the agent.
 */
const SECRET = randomBytes(32);
const DEFAULT_TTL_MS = 5 * 60 * 1000;
function b64url(buf) {
    return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
function b64urlDecode(s) {
    const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - (s.length % 4));
    return Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/") + pad, "base64");
}
/** Mint a signed filter token for the given selection context. */
export function mintFilterToken(input) {
    const payload = {
        filter: input.filter,
        agentType: input.agentType,
        accountId: input.accountId,
        issuedAt: Date.now(),
        nonce: b64url(randomBytes(12)),
    };
    const payloadJson = Buffer.from(JSON.stringify(payload), "utf8");
    const sig = createHmac("sha256", SECRET).update(payloadJson).digest();
    return `${b64url(payloadJson)}.${b64url(sig)}`;
}
/**
 * Verify a filter token. Returns the payload when signature+TTL pass; returns
 * a structured rejection reason otherwise. Callers must treat a rejection as
 * load-bearing — never silently fall back to "delete anyway."
 */
export function verifyFilterToken(token, opts) {
    const ttlMs = opts?.ttlMs ?? DEFAULT_TTL_MS;
    const now = opts?.now ?? Date.now();
    const parts = token.split(".");
    if (parts.length !== 2)
        return { ok: false, reason: "malformed" };
    let payload;
    let payloadJson;
    try {
        payloadJson = b64urlDecode(parts[0]);
        payload = JSON.parse(payloadJson.toString("utf8"));
    }
    catch {
        return { ok: false, reason: "malformed" };
    }
    const expected = createHmac("sha256", SECRET).update(payloadJson).digest();
    const actual = b64urlDecode(parts[1]);
    if (expected.length !== actual.length)
        return { ok: false, reason: "bad-signature" };
    let diff = 0;
    for (let i = 0; i < expected.length; i++)
        diff |= expected[i] ^ actual[i];
    if (diff !== 0)
        return { ok: false, reason: "bad-signature" };
    if (now - payload.issuedAt > ttlMs)
        return { ok: false, reason: "expired" };
    return { ok: true, payload };
}
/**
 * Short hash of the token for log correlation — never logs the raw token.
 * Use in `[memory-delete] filterToken=<sha8>` emissions.
 */
export function filterTokenFingerprint(token) {
    return createHmac("sha256", SECRET).update(token).digest("hex").slice(0, 8);
}
//# sourceMappingURL=filter-token.js.map