import { KeyedStore } from "./keyed-store.js";
//#region src/infra/rate-limit/failure-limiter.ts
/**
* Per-key brute-force protection limiter.
*
* Semantics:
*   - `fail(key)` records a failed attempt; reaching `maxFailures` within
*     `windowMs` triggers a `blockDurationMs` lockout.
*   - `succeed(key)` clears all state for the key (a successful auth wipes the
*     bucket so legitimate users aren't punished for prior typos).
*   - `check(key)` is read-only — returns the current block status.
*   - Failures from the same key within `burstCoalesceMs` collapse to a single
*     attempt. This absorbs SPA fan-out / SDK auto-retry storms without
*     weakening protection: a deliberate attacker is rate-limited to ~1
*     attempt/s per key, still astronomically slow against any token of
*     meaningful entropy.
*
* Pure counter — no policy knowledge (loopback exemption, key formatting,
* etc. all live in the policy layer that calls into this limiter).
*/
const DEFAULT_BURST_COALESCE_MS = 1e3;
var FailureLimiter = class {
	store;
	clock;
	maxFailures;
	windowMs;
	blockDurationMs;
	burstCoalesceMs;
	constructor(cfg) {
		this.maxFailures = Math.max(1, Math.floor(cfg.maxFailures));
		this.windowMs = Math.max(1e3, Math.floor(cfg.windowMs));
		this.blockDurationMs = Math.max(1e3, Math.floor(cfg.blockDurationMs));
		this.burstCoalesceMs = Math.max(0, Math.floor(cfg.burstCoalesceMs ?? DEFAULT_BURST_COALESCE_MS));
		this.clock = cfg.clock ?? Date.now;
		this.store = new KeyedStore({
			staleAfterMs: cfg.staleAfterMs ?? this.windowMs + this.blockDurationMs,
			clock: this.clock
		});
	}
	check(key) {
		const now = this.clock();
		const state = this.store.get(key);
		if (!state?.blockedUntil) return { blocked: false };
		if (now >= state.blockedUntil) {
			state.blockedUntil = void 0;
			state.count = 0;
			state.windowStart = now;
			state.lastTouchedMs = now;
			return { blocked: false };
		}
		return {
			blocked: true,
			retryAfterSec: Math.max(1, Math.ceil((state.blockedUntil - now) / 1e3))
		};
	}
	fail(key) {
		const now = this.clock();
		let state = this.store.get(key);
		if (!state) {
			state = {
				windowStart: now,
				count: 0,
				lastTouchedMs: now
			};
			this.store.set(key, state);
		}
		state.lastTouchedMs = now;
		if (state.blockedUntil && now < state.blockedUntil) return;
		if (state.blockedUntil && now >= state.blockedUntil) {
			state.blockedUntil = void 0;
			state.count = 0;
			state.windowStart = now;
		}
		if (now - state.windowStart > this.windowMs) {
			state.count = 0;
			state.windowStart = now;
		}
		if (state.lastFailureAtMs !== void 0 && now - state.lastFailureAtMs < this.burstCoalesceMs) {
			state.lastFailureAtMs = now;
			return;
		}
		state.lastFailureAtMs = now;
		state.count += 1;
		if (state.count >= this.maxFailures) state.blockedUntil = now + this.blockDurationMs;
	}
	succeed(key) {
		this.store.delete(key);
	}
	/** @internal Test hook. */
	resetForTests() {
		this.store.clear();
	}
	destroy() {
		this.store.destroy();
	}
};
//#endregion
export { FailureLimiter };

//# sourceMappingURL=failure-limiter.js.map