import { T as ThrottleKitError } from './quota-C4WEn9R6.js'; import { LimiterSpec } from './config.js'; import { M as ManualClock } from './clock-CnB6yaAt.js'; import { d as Strategy, D as Decision, L as Limiter } from './types-DKirIBQt.js'; /** * Why a replay was refused. Replay never silently returns a misleading result — when a precondition * is violated it throws a {@link ReplayRefusedError} carrying one of these. Each value is a distinct, * fail-loud hazard (design §4.5 / §9): * * - `trace-format-version` — a serialized trace from an incompatible {@link TRACE_FORMAT_VERSION}. * - `trace-malformed` — a version-compatible trace whose structure is invalid (e.g. `steps` is * not an array, a step is missing fields, or a Decision field is non-finite). * A parsed/transmitted trace is untrusted input; replay refuses it rather * than failing open to a misleading "zero divergence". * - `trace-empty` — the trace has no steps to replay. * - `trace-truncated` — recording hit its cap and dropped steps; a what-if over a prefix would * understate the effect, so the partial trace is refused (re-record larger). * - `unrebuildable-strategy` — the spec's strategy is not constructible by `buildStrategy` * (e.g. `leakyBucket`, or a composite/unknown strategy). * - `non-manual-clock` — the recording ran over the system clock or a Redis server clock, so its * instants are not deterministically reproducible. * - `lua-sha1-mismatch` — the rebuilt strategy's Lua differs from what was recorded (build drift): * the rebuild is not the strategy that produced the trace. * - `strategy-mismatch` — the rebuilt strategy's identity (name/limit/window) ≠ the recorded * fingerprint (a tampered or mislabelled trace). * - `unreplayable-axis` — a non-rate admission axis (concurrency: releases are not decisions, so a * decision trace cannot reproduce them). * - `unreplayable-policy` — a joint-LP admission policy (a bid-price filter, not a leaf decision). * - `keyref-collision` — a redaction hook mapped two distinct keys to one value, which would merge * their state and corrupt the replay. * - `identity-divergence` — the identity self-check failed: replaying the recorded spec did not * reproduce the recording bit-for-bit, so the determinism substrate is broken. * - `candidate-invalid` — a P2 candidate delta is ill-formed (unknown field, more than one op on a * field, a non-numeric `scale` base) or produced a spec `buildStrategy` cannot * construct (e.g. a `swap` missing the new strategy's required fields). The * scorecard surfaces this as a loud per-candidate `refused` row, never a * silent zero-change result. */ type ReplayRefusal = "trace-format-version" | "trace-malformed" | "trace-empty" | "trace-truncated" | "unrebuildable-strategy" | "non-manual-clock" | "lua-sha1-mismatch" | "strategy-mismatch" | "unreplayable-axis" | "unreplayable-policy" | "keyref-collision" | "identity-divergence" | "candidate-invalid"; /** * A replay precondition was violated. Carries a machine-readable {@link ReplayRefusal} `reason` — * prefer it over message matching. The base `code` is `"config_invalid"`: a refusal is, at root, a * statement that the trace/spec is not a valid replay input. * * @experimental Part of the opt-in replay testkit (see STABILITY.md). */ declare class ReplayRefusedError extends ThrottleKitError { /** The specific precondition that was violated. */ readonly reason: ReplayRefusal; constructor(reason: ReplayRefusal, message: string); } /** * Which clock a recording ran on. Only `"manual"` (a {@link ManualClock}) is deterministically * replayable — a `"system"` (wall-clock) or `"server"` (Redis `TIME`) recording records instants * that cannot be reproduced, and is refused at replay. The non-manual values exist so the fingerprint * type can faithfully *represent* such a recording (e.g. a future server-side capture) and refuse it. */ type ReplayClockSource = "manual" | "system" | "server"; /** * The one admission axis a *decision* trace can replay. Concurrency is intentionally excluded: * releases are not decisions, so a concurrency limit's behaviour cannot be reconstructed from a * trace of admit/deny decisions. A non-`"rate"` value is refused. */ type ReplayAxis = "rate"; /** Recorded strategy identity — a cheap structural cross-check against the spec-rebuilt strategy. */ interface StrategyIdentity { readonly name: string; readonly limit: number; readonly windowMs?: number; readonly ttlMs: number; } /** * Everything needed to rebuild — and to validate the rebuild of — the exact leaf limiter a trace was * recorded over. Captured once at record time; checked by `assertReplayable` before any replay. * * The `spec` is the rebuild input (re-run through the single source of truth, `buildStrategy`). The * other fields are guards: `strategy`/`luaSha1` catch a rebuild that drifted from (or doesn't match) * the recording; `clock`/`axis`/`policy` refuse a recording that is not deterministically replayable. */ interface ReplayFingerprint { /** The declarative spec the limiter was built from. */ readonly spec: LimiterSpec; /** Recorded strategy identity, cross-checked against the spec-rebuilt strategy. */ readonly strategy: StrategyIdentity; /** Clock the recording ran on; replay refuses anything but `"manual"`. */ readonly clock: ReplayClockSource; /** The replayable axis; always `"rate"` for a library recording. */ readonly axis: ReplayAxis; /** A non-null admission policy (e.g. `"joint-lp"`) is refused; `null` for a plain leaf limiter. */ readonly policy: string | null; /** SHA-1 of the strategy's Lua script when present (else `null`), so a drifted rebuild is caught. */ readonly luaSha1: string | null; /** Key prefix at record time, so the rebuild reproduces the exact store keys. */ readonly prefix?: string; } /** SHA-1 of a strategy's Lua program source, or `null` when the strategy carries no Lua form. */ declare function luaSha1(strategy: Strategy): string | null; /** * Build the fingerprint for a freshly-built recording limiter. The recorder always supplies * `clock: "manual"` (it requires a {@link ManualClock}); `axis`/`policy` are the safe leaf-rate * values. A trace that needs to represent a non-replayable recording sets those fields directly. */ declare function fingerprint(params: { spec: LimiterSpec; strategy: Strategy; clock: ReplayClockSource; prefix?: string; }): ReplayFingerprint; /** * Current on-disk trace format. A serialized trace from any other version is refused on parse * (fail-loud forward/backward compatibility): a stored trace must be re-recorded on a version bump * rather than silently mis-read. */ declare const TRACE_FORMAT_VERSION: 1; /** * One recorded decision: the exact `(key, cost, instant)` inputs and the {@link Decision} the * limiter produced. `at` is the absolute epoch-ms the {@link ManualClock} read at the check; replay * `set()`s the clock to it (absolute, never an accumulated delta), so coincident instants and any * ordering reproduce faithfully. */ interface ReplayStep { readonly key: string; readonly cost: number; readonly at: number; readonly decision: Decision; } /** * A self-contained, JSON-serializable decision trace: a {@link ReplayFingerprint} (everything needed * to rebuild the exact leaf limiter) plus the ordered decision {@link ReplayStep}s. Replay drives the * steps, in order, against a freshly-rebuilt **cold** limiter and checks the Decisions reproduce. */ interface ReplayTrace { /** Format version — see {@link TRACE_FORMAT_VERSION}. */ readonly version: typeof TRACE_FORMAT_VERSION; /** Rebuild + validation fingerprint. */ readonly fingerprint: ReplayFingerprint; /** Whether any recorded `key` was passed through a redaction hook (honest disclosure on a stored trace). */ readonly redacted: boolean; /** * True when recording stopped at its cap — the trace is a faithful **prefix**, not the whole run. * Replay refuses a truncated trace (a what-if over a prefix understates the effect). */ readonly truncated: boolean; /** Steps dropped after the cap (`0` unless truncated). For honest reporting only. */ readonly dropped: number; /** The ordered decision steps. */ readonly steps: readonly ReplayStep[]; } /** Serialize a trace to JSON (it is plain data — `Decision`s and a declarative spec). */ declare function serializeTrace(trace: ReplayTrace): string; /** * Parse a serialized trace, refusing any incompatible {@link TRACE_FORMAT_VERSION} (fail-loud). This * is a structural gate on the version envelope, not a deep schema validation: a trace produced by a * compatible version is trusted, an incompatible one is rejected with a clear instruction. */ declare function parseTrace(text: string): ReplayTrace; /** * Structurally validate a (version-compatible) trace before any replay trusts it. A trace that was * serialized, transmitted, or hand-built is **untrusted input**: without this check a non-array * `steps` reads `steps.length === undefined`, slips past the empty/loop guards, and produces a * misleading "zero divergence" result instead of a refusal. This is the trust boundary — it refuses * (`trace-malformed`) anything the downstream guards and the engine's `drive`/`divergence` would * otherwise misread. A trace from {@link recordLimiter} is always well-formed, so the happy path is a * cheap pass. */ declare function assertWellFormedTrace(trace: ReplayTrace): void; interface RecordOptions { /** * The {@link ManualClock} the recording is driven by — you advance it between checks to simulate * arrivals. Default: a fresh `ManualClock(0)`, exposed as {@link Recording.clock}. Must be a * `ManualClock`: a system/server clock records non-reproducible instants. */ readonly clock?: ManualClock; /** Key prefix for the underlying limiter (default: the config name). */ readonly prefix?: string; /** Config name — `buildStrategy` error context + labelling. Default `"recorded"`. */ readonly name?: string; /** * Cap on recorded steps. At the cap recording stops appending: the kept **prefix** stays a faithful * recording, but the trace is flagged `truncated` and replay refuses it (re-record larger). The cap * is a tail-stop, deliberately **not** a drop-oldest ring — dropping the oldest steps would lose the * cold-start prefix replay needs to rebuild state. Default 1,000,000. */ readonly maxSteps?: number; /** * OFF by default (identity). Redact each key **at capture**, so the trace stores only redacted keys * and replay (which rebuilds from them) stays faithful. A redaction that maps two distinct keys to * the same value is refused (`keyref-collision`) — silently merging their state would corrupt the * replay. Supply e.g. a salted hash; mind that a hash trades a small collision risk for privacy. */ readonly redactKey?: (key: string) => string; } interface Recording { /** * The recording limiter. Call `checkSync` / `checkManySync`; each decision appends a step at the * clock's current instant. The async `check` / `checkMany` and `reset` are refused: recording is * synchronous-only (an async check would not be captured deterministically) and a reset would * desynchronize the decision trace from replay. */ readonly limiter: Limiter; /** The {@link ManualClock} the recording is driven by — advance it to simulate arrivals. */ readonly clock: ManualClock; /** Snapshot the immutable trace recorded so far. */ trace(): ReplayTrace; } /** * Wrap a leaf limiter — built from `spec`, so the trace's fingerprint provably rebuilds it — and * record every synchronous decision into a bounded {@link ReplayTrace}. The recording limiter is * constructed by {@link rebuildLimiter}, the exact deterministic construction replay uses * (`MemoryStore`, `sweepIntervalMs: 0`, shared `ManualClock`), so a recording and its replay start * from the same cold state and evolve identically. * * @experimental Part of the opt-in replay testkit (see STABILITY.md). */ declare function recordLimiter(spec: LimiterSpec, options?: RecordOptions): Recording; export { type RecordOptions as R, type StrategyIdentity as S, TRACE_FORMAT_VERSION as T, type Recording as a, type ReplayAxis as b, type ReplayClockSource as c, type ReplayFingerprint as d, type ReplayRefusal as e, ReplayRefusedError as f, type ReplayStep as g, type ReplayTrace as h, assertWellFormedTrace as i, fingerprint as j, luaSha1 as l, parseTrace as p, recordLimiter as r, serializeTrace as s };