Files
suaveplan/packages/networking/network/websocket-gateway/src/gateway/access-gate.md
2026-05-27 23:47:53 +01:00

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_LIMITED and AUTH_REJECTED codes 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 injected AuditLogger.
  • 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. Throws WsGatewayError on 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.