feat(live): task 1.5.3 — subscription registry & per-event authorization
Subscribe/unsubscribe with per-event authorization via Directus delegation: - src/live/authz.ts: createAuthzClient factory; canAccessEvent(cookieHeader, eventId) calls GET /items/events/<id>?fields=id, delegates row-level security to Directus (200=allow, 403=forbidden, 404=not-found, else error). - src/live/registry.ts: createSubscriptionRegistry with bidirectional indexes (WeakMap<conn, topics> + Map<topic, conns>); subscribe/unsubscribe/ onConnectionClose/connectionsForTopic/topicsForConnection/stats. Authorization runs once at subscribe time. Snapshot is stubbed as [] until task 1.5.5. Includes pluggable SnapshotProvider interface for task 1.5.5 injection. - src/live/protocol.ts: adds 'error' to ErrorCode union for transient authz failures. - src/main.ts: wires createAuthzClient + createSubscriptionRegistry; replaces the stub message handler with the real subscribe/unsubscribe router; passes registry.onConnectionClose as the server's onClose callback. - test/live-authz.test.ts: 6 unit tests for all canAccessEvent outcomes. - test/live-registry.test.ts: 9 unit tests for subscribe/unsubscribe semantics, idempotency, gauge correctness, and onConnectionClose cleanup.
This commit is contained in:
+15
-14
@@ -16,10 +16,12 @@ import { connectRedis, createConsumer } from './core/consumer.js';
|
||||
import type { ConsumedRecord } from './core/consumer.js';
|
||||
import { createDeviceStateStore } from './core/state.js';
|
||||
import { createWriter } from './core/writer.js';
|
||||
import { createLiveServer, sendOutbound } from './live/server.js';
|
||||
import { createLiveServer } from './live/server.js';
|
||||
import type { LiveServer, LiveConnection } from './live/server.js';
|
||||
import type { InboundMessage } from './live/protocol.js';
|
||||
import { createAuthClient } from './live/auth.js';
|
||||
import { createAuthzClient } from './live/authz.js';
|
||||
import { createSubscriptionRegistry } from './live/registry.js';
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Startup: validate config (fail fast on bad env), build logger
|
||||
@@ -132,29 +134,28 @@ async function main(): Promise<void> {
|
||||
return ackIds;
|
||||
};
|
||||
|
||||
// 10. Build the live WebSocket server (task 1.5.2 adds auth).
|
||||
// The stub message handler replies with `error/not-implemented` until
|
||||
// task 1.5.3 wires in the real subscription-registry handler.
|
||||
// 10. Build the live WebSocket server (tasks 1.5.2 and 1.5.3).
|
||||
const authClient = createAuthClient(config, logger, metrics);
|
||||
const authzClient = createAuthzClient(config, logger, metrics);
|
||||
const registry = createSubscriptionRegistry(authzClient, config, logger, metrics);
|
||||
|
||||
const stubMessageHandler = async (
|
||||
const messageHandler = async (
|
||||
conn: LiveConnection,
|
||||
_message: InboundMessage,
|
||||
message: InboundMessage,
|
||||
): Promise<void> => {
|
||||
sendOutbound(
|
||||
conn,
|
||||
{ type: 'error', code: 'not-implemented' },
|
||||
metrics,
|
||||
config.LIVE_WS_BACKPRESSURE_THRESHOLD_BYTES,
|
||||
);
|
||||
if (message.type === 'subscribe') {
|
||||
await registry.subscribe(conn, message.topic, message.id);
|
||||
} else if (message.type === 'unsubscribe') {
|
||||
registry.unsubscribe(conn, message.topic, message.id);
|
||||
}
|
||||
};
|
||||
|
||||
const liveServer: LiveServer = createLiveServer(
|
||||
config,
|
||||
logger,
|
||||
metrics,
|
||||
stubMessageHandler,
|
||||
undefined, // onClose: wired in task 1.5.3
|
||||
messageHandler,
|
||||
(conn) => registry.onConnectionClose(conn),
|
||||
authClient,
|
||||
);
|
||||
await liveServer.start();
|
||||
|
||||
Reference in New Issue
Block a user