fix(gateway): default non-finite auth guard limits

This commit is contained in:
Peter Steinberger
2026-05-29 00:58:16 -04:00
parent 1d11178d02
commit 8ada0f4ae2
4 changed files with 51 additions and 4 deletions

View File

@@ -47,6 +47,31 @@ describe("HandshakeAuthLogLimiter", () => {
});
});
it("uses default limits for non-finite options", () => {
const limiter = new HandshakeAuthLogLimiter({
intervalMs: Number.NaN,
maxEntries: Number.POSITIVE_INFINITY,
});
expect(limiter.register("first", 0)).toEqual({
shouldLog: true,
suppressedSinceLastLog: 0,
});
expect(limiter.register("first", 1_000)).toEqual({
shouldLog: false,
suppressedSinceLastLog: 0,
});
for (let i = 0; i < 256; i += 1) {
limiter.register(`key-${i}`, i);
}
expect(limiter.register("first", 2_000)).toEqual({
shouldLog: true,
suppressedSinceLastLog: 0,
});
});
it("only rate-limits benign missing-credential startup retries", () => {
expect(
shouldLimitMissingCredentialAuthLog({

View File

@@ -1,3 +1,5 @@
import { resolveIntegerOption } from "../../../shared/number-coercion.js";
export type HandshakeAuthLogDecision = {
shouldLog: boolean;
suppressedSinceLastLog: number;
@@ -14,8 +16,8 @@ export class HandshakeAuthLogLimiter {
private readonly entries = new Map<string, HandshakeAuthLogState>();
constructor(options?: { intervalMs?: number; maxEntries?: number }) {
this.intervalMs = Math.max(1, Math.floor(options?.intervalMs ?? 30_000));
this.maxEntries = Math.max(1, Math.floor(options?.maxEntries ?? 256));
this.intervalMs = resolveIntegerOption(options?.intervalMs, 30_000, { min: 1 });
this.maxEntries = resolveIntegerOption(options?.maxEntries, 256, { min: 1 });
}
register(key: string, nowMs = Date.now()): HandshakeAuthLogDecision {

View File

@@ -31,6 +31,25 @@ describe("UnauthorizedFloodGuard", () => {
});
});
it("uses default thresholds for non-finite options", () => {
const guard = new UnauthorizedFloodGuard({
closeAfter: Number.NaN,
logEvery: Number.POSITIVE_INFINITY,
});
for (let i = 0; i < 10; i += 1) {
expect(guard.registerUnauthorized().shouldClose).toBe(false);
}
const eleventh = guard.registerUnauthorized();
expect(eleventh).toMatchObject({
shouldClose: true,
shouldLog: true,
count: 11,
suppressedSinceLastLog: 9,
});
});
it("resets counters", () => {
const guard = new UnauthorizedFloodGuard({ closeAfter: 10, logEvery: 50 });
guard.registerUnauthorized();

View File

@@ -1,4 +1,5 @@
import { ErrorCodes, type ErrorShape } from "../../../../packages/gateway-protocol/src/index.js";
import { resolveIntegerOption } from "../../../shared/number-coercion.js";
export type UnauthorizedFloodGuardOptions = {
closeAfter?: number;
@@ -22,8 +23,8 @@ export class UnauthorizedFloodGuard {
private suppressedSinceLastLog = 0;
constructor(options?: UnauthorizedFloodGuardOptions) {
this.closeAfter = Math.max(1, Math.floor(options?.closeAfter ?? DEFAULT_CLOSE_AFTER));
this.logEvery = Math.max(1, Math.floor(options?.logEvery ?? DEFAULT_LOG_EVERY));
this.closeAfter = resolveIntegerOption(options?.closeAfter, DEFAULT_CLOSE_AFTER, { min: 1 });
this.logEvery = resolveIntegerOption(options?.logEvery, DEFAULT_LOG_EVERY, { min: 1 });
}
registerUnauthorized(): UnauthorizedFloodDecision {