Five fixes surfaced by the branch-wide code review on the realtime layer:
- server.ts: replace dynamic `import("@repo/auth/di/container")` with a
static top-of-file import. The dynamic-import workaround from 6a0ac63 is
no longer needed once the root tsconfig + TSX_TSCONFIG_PATH expose
decorator metadata to tsx; verified by booting `pnpm dev` clean.
- server.ts: correct the inline structural type for `validateSession` to
match the real `IAuthenticationService` contract (non-nullable, throws
on invalid session) and wrap the call in try/catch so unauthenticated
bubbles to a `null` return instead of dead-code `result ? ... : null`.
- bind-production.ts: extract `maybeRegisterRealtimePing()` that wraps the
built-in ping inbound handler in the same `withSpan(withCapture(...))`
sandwich the realtime-handler generator emits (R41–R44), so the
proof-of-life channel models the convention rather than registering raw.
- bind-production.test.ts: add 4 tests for the `REALTIME_PING_DISABLED`
env-gate (registered when unset in both binders, not registered when
"true", treated as enabled when "1").
- docs/guides/realtime.md: correct the integration-test reference at
line 285 — the test does not call `bindAllDevSeed()`; it builds the
Socket.IO server inline and exercises gates 1+2 only (gates 3+4 live in
socket-io-realtime-server.test.ts).
- adr-016: add a "Known follow-ups" section recording 6 lower-priority
refinements deferred from this branch (bridge stub test scaffolding,
registry register/registerChannel precedence, channel-template dot
constraint, server bare catch{}, BindAllDeps Partial widening, AGENTS.md
anchor count phrasing).
CI gates: lint 0 errors / 4 warnings (pre-existing turbo.json warnings),
typecheck clean, 24 web-next tests pass (was 20; 4 new env-gate tests),
boundaries 0 issues across 504 files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
70 lines
2.4 KiB
TypeScript
70 lines
2.4 KiB
TypeScript
// apps/web-next/server.ts
|
|
// SERVER-ONLY entry. Boots Next.js + Socket.IO on the same Node http server.
|
|
import "reflect-metadata";
|
|
import { createServer } from "node:http";
|
|
import next from "next";
|
|
import { Server as IOServer } from "socket.io";
|
|
import {
|
|
RealtimeHandlerRegistry,
|
|
SocketIORealtimeBroadcaster,
|
|
SocketIORealtimeServer,
|
|
type IRealtimeAuthenticator,
|
|
} from "@repo/core-realtime";
|
|
import { SESSION_COOKIE } from "@repo/auth";
|
|
import { authContainer } from "@repo/auth/di/container";
|
|
import { AUTH_SYMBOLS } from "@repo/auth/di/symbols";
|
|
import { bindAll } from "./src/server/bind-production.js";
|
|
|
|
// Real shape of IAuthenticationService.validateSession: returns non-nullable
|
|
// on success and throws UnauthenticatedError on missing/invalid sessions.
|
|
// Kept as an inline structural type to avoid leaking auth's internal interface.
|
|
type AuthService = {
|
|
validateSession: (id: string) => Promise<{ user: { id: string }; session: unknown }>;
|
|
};
|
|
|
|
const dev = process.env.NODE_ENV !== "production";
|
|
const port = Number(process.env.PORT ?? 3000);
|
|
|
|
const app = next({ dev });
|
|
const handle = app.getRequestHandler();
|
|
|
|
await app.prepare();
|
|
|
|
const httpServer = createServer((req, res) => handle(req, res));
|
|
const io = new IOServer(httpServer);
|
|
|
|
const broadcaster = new SocketIORealtimeBroadcaster(io);
|
|
const registry = new RealtimeHandlerRegistry();
|
|
|
|
await bindAll({ realtime: broadcaster, realtimeRegistry: registry });
|
|
|
|
const authenticator: IRealtimeAuthenticator = {
|
|
authenticate: async ({ cookies }) => {
|
|
const sessionId = cookies[SESSION_COOKIE];
|
|
if (!sessionId) return null;
|
|
const authService = authContainer.get<AuthService>(AUTH_SYMBOLS.IAuthenticationService);
|
|
try {
|
|
const { user } = await authService.validateSession(sessionId);
|
|
// Roles are not yet in the session shape; extend here when DB-backed roles ship.
|
|
return { userId: user.id, roles: (user as { roles?: string[] }).roles ?? [] };
|
|
} catch {
|
|
// Invalid/expired session → reject the connection. Real auth-service errors
|
|
// (DB outages etc.) intentionally collapse to "unauthenticated" here too,
|
|
// which is the conservative choice for a public-facing socket.
|
|
return null;
|
|
}
|
|
},
|
|
};
|
|
|
|
const realtimeServer = new SocketIORealtimeServer({
|
|
httpServer,
|
|
io,
|
|
authenticator,
|
|
registry,
|
|
});
|
|
await realtimeServer.start();
|
|
|
|
httpServer.listen(port, () => {
|
|
console.log(`> Ready on http://localhost:${port}`);
|
|
});
|