/** * MCP Client-Assertion Replay Guard (RFC 7523 §3 `jti`) * * Records seen client-assertion `jti` values in the `mcp_assertion_jtis` * Harper table so a captured assertion cannot be redeemed twice (#159 security * req 1 — a timestamp-only window is insufficient). The table carries * `expiration: 120`, comfortably past the maximum assertion window (60s `exp` * + clock tolerance), so rows self-evict; runtime never needs to prune. * * Keys are `sha256(len:client_id:jti)` — the client_id is length-prefixed so * the component boundary is unambiguous (plain delimiter concatenation lets * crafted inputs collide). Replay scope is per client (RFC 7523 defines `jti` * uniqueness per issuer), and hashing normalizes an arbitrary client-chosen * string to a fixed-length key. * * Enforcement uses `Table.create()` — Harper's insert-if-absent, which throws * a 409 ClientError ("Record already exists") — rather than an awaited * get-then-put a concurrent request could interleave. * * Residual race (documented, accepted): Harper currently enforces create()'s * existence check against the pre-staging snapshot only, so concurrent * in-flight creates can degrade to last-write-wins with both callers * reporting success (HarperFast/harper#1745). Exposure is bounded to * presentations in flight simultaneously — within the staging→commit interval * on one node, or replication lag across nodes — NOT open reuse across the * assertion's ~60s validity window; each duplicate mints one short-TTL token. * If harper#1745 lands per-node enforcement, the 409 also surfaces on * commit-time conflict and this guard becomes fully atomic per node with no * code change here (the catch below already handles it). Do NOT replace this * table with a per-process cache, which would not be shared across workers * or nodes. * * Unlike the other MCP stores, storage errors here are NOT swallowed: * treating "could not check" as "not seen" would fail open on the one guard * whose whole job is rejecting repeats. Callers should map a throw to a 500. */ import type { Logger } from '../../types.ts'; /** * Reset the cached table reference (for testing only) * @internal */ export declare function resetMCPAssertionJtisTableCache(): void; /** * Fixed-length, charset-safe primary key for a (client, jti) sighting. The * client_id is length-prefixed so the component boundary is unambiguous — * plain concatenation with a delimiter would let ("a", "b\nc") and * ("a\nb", "c") collide. */ export declare function jtiKey(clientId: string, jti: string): string; export declare class MCPAssertionJtiStore { private logger?; constructor(logger?: Logger); /** * Record a (client_id, jti) sighting. Returns true when this is the first * sighting (proceed with issuance), false when the jti was already seen * (replay — reject). Non-409 storage errors propagate to the caller: this * guard must fail closed, never "couldn't check, assume fresh". */ checkAndRecord(clientId: string, jti: string): Promise; }