2.3 KiB
gate + audit
Purpose
Central rate-limit / authorization / audit pipeline. The gateway calls gate(...) once before every mutating action (room create, broadcast, channel subscribe, channel publish, presence update, unicast). Splitting this out of the main daemon file keeps gateway.ts under the 300-line cap and makes the gate behaviour unit-testable without booting a server.
Features
- Strict ordering: rate-limit first, authorization second. DoS traffic never exercises the (potentially expensive) authorization callback.
- Typed denials:
RATE_LIMITEDandAUTH_REJECTEDcodes surface the exact failure reason to the caller. - Every decision records an OTel
recordAction(action, outcome)counter increment AND a structured audit-log entry through the injectedAuditLogger. audit(...)swallows any thrown logger error so observability bugs cannot crash the daemon.
Usage: Basic Example
import { gate } from "@suaveplan/websocket-gateway";
await gate(
{ rateLimitGate, authorizationCheck, auditLogger, clock: Date.now },
ctx,
"room.broadcast"
);
Usage: Advanced Example
import { audit, gate } from "@suaveplan/websocket-gateway";
try {
await gate(deps, ctx, "channel.publish");
// … perform the publish
} catch (err) {
if (err instanceof WsGatewayError && err.code === WS_GATEWAY_CODES.RATE_LIMITED) {
// The audit logger has already received a "denied" record; the
// caller may emit an additional client-side denial event.
audit(deps.auditLogger, "channel.publish", ctx, "denied", "client-handled", Date.now());
}
throw err;
}
API Reference
gate(deps, ctx, action): Promise<void>— runs rate-limit + authz. ThrowsWsGatewayErroron denial.audit(logger, action, ctx, outcome, reason, at): void— pushes one record through the audit logger; swallows errors.
Implementation Notes
The function is intentionally a free function (not a class method) so the daemon can pass it any composition of callbacks — a future tier might want to gate-only without auditing, or audit-only without gating. Decoupling the four hooks from each other matches the proposal's "Hooks for unmigrated deps" stance: each callback is replaceable independently when the matching @suaveplan/* package lands.