/** * T7 — sequence guard (anti-replay/injection, INV13). * * The transport (§4.1 mux over an ordered WS/TCP stream) delivers per-stream in order, so INV13 is * enforced as STRICT SUCCESSOR: recv requires `seq === last + 1` (from 0). A duplicate, reorder, * gap, or rewind all raise `ReplayError`. There is deliberately NO sliding acceptance window. */ import { ReplayError } from './errors.js' const U64_MAX = 2n ** 64n - 1n export class SequenceGuard { readonly role: 'send' | 'recv' #next: bigint = 0n #lastAccepted: bigint | null = null constructor(role: 'send' | 'recv') { this.role = role } /** send-side: return the current seq, then increment (strictly monotonic per direction). */ next(): bigint { if (this.role !== 'send') { throw new ReplayError('next() called on a recv guard') } const seq = this.#next if (seq > U64_MAX) { throw new ReplayError('sequence exhausted u64 — re-key required') } this.#next += 1n return seq } /** recv-side: require the strict successor; else throw ReplayError (dup/reorder/gap/rewind). */ accept(seq: bigint): void { if (this.role !== 'recv') { throw new ReplayError('accept() called on a send guard') } if (seq < 0n || seq > U64_MAX) { throw new ReplayError(`sequence out of u64 range: ${seq}`) } const expected = this.#lastAccepted === null ? 0n : this.#lastAccepted + 1n if (seq !== expected) { throw new ReplayError(`out-of-order sequence: expected ${expected}, got ${seq}`) } this.#lastAccepted = seq } get lastAccepted(): bigint | null { return this.#lastAccepted } /** Next expected recv seq (send guards report their next send seq). */ get expected(): bigint { return this.role === 'send' ? this.#next : this.#lastAccepted === null ? 0n : this.#lastAccepted + 1n } }