feat(live): task 1.5.2 — cookie auth handshake

Authenticate WebSocket upgrade requests via Directus's /users/me:
- src/live/auth.ts: createAuthClient factory; validate() forwards the raw
  Cookie: header to Directus, parses the user with zod, and returns
  AuthenticatedUser or null. Handles 401/403 (unauthorized), non-2xx (error),
  network failures, AbortError (timeout), null data (expired session), and
  missing data key (malformed Directus response).
- src/live/server.ts: upgrade handler now calls authClient.validate() before
  completing the WS handshake; on null user, writes HTTP 401 and destroys the
  socket. LiveConnection gains user: AuthenticatedUser and cookieHeader: string
  (needed for per-subscription authz in task 1.5.3). authClient is an optional
  parameter so tests without auth still work.
- src/main.ts: wires createAuthClient and passes it to createLiveServer.
- test/live-auth.test.ts: 11 unit tests covering all validate() code paths
  including the empty-cookie fast-path, latency histogram observation, and
  distinction between unauthorized (401/expired) and error (malformed) results.
This commit is contained in:
2026-05-02 17:36:28 +02:00
parent 8a78e53e58
commit 20ebd9b473
4 changed files with 513 additions and 16 deletions
+7 -2
View File
@@ -19,6 +19,7 @@ import { createWriter } from './core/writer.js';
import { createLiveServer, sendOutbound } 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';
// -------------------------------------------------------------------------
// Startup: validate config (fail fast on bad env), build logger
@@ -131,9 +132,11 @@ async function main(): Promise<void> {
return ackIds;
};
// 10. Build the live WebSocket server (task 1.5.1).
// 10. Build the live WebSocket server (task 1.5.2 adds auth).
// The stub message handler replies with `error/not-implemented` until
// tasks 1.5.2 and 1.5.3 wire in the real auth + registry handler.
// task 1.5.3 wires in the real subscription-registry handler.
const authClient = createAuthClient(config, logger, metrics);
const stubMessageHandler = async (
conn: LiveConnection,
_message: InboundMessage,
@@ -151,6 +154,8 @@ async function main(): Promise<void> {
logger,
metrics,
stubMessageHandler,
undefined, // onClose: wired in task 1.5.3
authClient,
);
await liveServer.start();