Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
# `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
```ts
import { gate } from "@suaveplan/websocket-gateway";
await gate(
{ rateLimitGate, authorizationCheck, auditLogger, clock: Date.now },
ctx,
"room.broadcast"
);
```
## Usage: Advanced Example
```ts
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.