2026-08-16 13:25:10 +07:00
|
|
|
import { nodeHTTPRequestHandler } from "@trpc/server/adapters/node-http";
|
2026-06-01 21:44:29 +07:00
|
|
|
import express, {
|
|
|
|
|
type Express,
|
|
|
|
|
type NextFunction,
|
|
|
|
|
type Request,
|
|
|
|
|
type Response,
|
|
|
|
|
} from "express";
|
|
|
|
|
import helmet from "helmet";
|
2026-08-01 22:09:02 +07:00
|
|
|
import { createChildLogger } from "@/shared/logger/index";
|
2026-07-27 21:54:31 +07:00
|
|
|
import { createHealthRouter } from "../modules/health/index.js";
|
2026-07-26 11:33:45 +07:00
|
|
|
import { errorHandler } from "../shared/middlewares/index.js";
|
2026-08-16 13:25:10 +07:00
|
|
|
import { appRouter } from "../trpc/routers";
|
2026-06-13 14:10:34 +07:00
|
|
|
|
2026-08-16 13:25:10 +07:00
|
|
|
// Auth removed — dashboard is public.
|
|
|
|
|
// All data APIs (dashboard, messages, moderation, media, voice, recordings,
|
|
|
|
|
// analysis, chatbot, config, ui-state) now flow over tRPC, served on TWO
|
|
|
|
|
// transports sharing the /trpc path:
|
|
|
|
|
// - WebSocket (browser live RPCs) — see trpc/ws.ts
|
|
|
|
|
// - HTTP POST (server-side / RSC fetch) — handled below
|
|
|
|
|
// Only infra endpoints (health, prometheus metrics) remain plain HTTP.
|
2026-07-02 03:54:44 +07:00
|
|
|
|
2026-06-01 21:44:29 +07:00
|
|
|
const logger = createChildLogger("http.app");
|
|
|
|
|
|
|
|
|
|
export function createHttpApp(): Express {
|
|
|
|
|
const app = express();
|
|
|
|
|
|
|
|
|
|
// Security middleware
|
|
|
|
|
app.use(
|
|
|
|
|
helmet({
|
|
|
|
|
contentSecurityPolicy: false,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
2026-08-16 13:25:10 +07:00
|
|
|
// Body parsing (still needed for any JSON POST; tRPC is WS-based)
|
2026-06-01 21:44:29 +07:00
|
|
|
app.use(express.json());
|
|
|
|
|
app.use(express.urlencoded({ extended: true }));
|
|
|
|
|
|
|
|
|
|
// Request logging
|
|
|
|
|
app.use((req: Request, res: Response, next: NextFunction) => {
|
|
|
|
|
if (req.path.startsWith("/api/")) {
|
|
|
|
|
res.set("Cache-Control", "no-store");
|
|
|
|
|
}
|
|
|
|
|
res.on("finish", () => {
|
|
|
|
|
if (req.originalUrl.startsWith("/.well-known/")) return;
|
|
|
|
|
if (req.originalUrl === "/favicon.ico") return;
|
|
|
|
|
if (res.statusCode >= 400) {
|
|
|
|
|
logger.warn(
|
|
|
|
|
{
|
|
|
|
|
method: req.method,
|
|
|
|
|
url: req.originalUrl,
|
|
|
|
|
statusCode: res.statusCode,
|
|
|
|
|
},
|
|
|
|
|
"HTTP request failed",
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
next();
|
|
|
|
|
});
|
|
|
|
|
|
2026-08-16 13:25:10 +07:00
|
|
|
// Infra-only HTTP endpoints
|
2026-06-01 21:44:29 +07:00
|
|
|
app.use("/api", createHealthRouter());
|
2026-08-16 13:25:10 +07:00
|
|
|
|
|
|
|
|
// tRPC over HTTP (server-side / RSC fetch). The context has no WebSocket
|
|
|
|
|
// here (that's the WS transport's job); procedures don't read ctx.conn, so
|
|
|
|
|
// a null conn is safe.
|
|
|
|
|
// NOTE: Express 5 (path-to-regexp v8) rejects the `"/trpc/*"` wildcard route,
|
|
|
|
|
// and `nodeHTTPRequestHandler` uses `opts.path` as the literal procedure
|
|
|
|
|
// path (it does NOT derive it from `req.url`). So we mount a plain
|
|
|
|
|
// middleware and compute the procedure path from the URL ourselves.
|
|
|
|
|
app.use((req: Request, res: Response, next: NextFunction) => {
|
|
|
|
|
if (!req.path.startsWith("/trpc")) {
|
|
|
|
|
next();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const procPath = req.url.replace(/^\/trpc\/?/, "").split("?")[0] || "/";
|
|
|
|
|
nodeHTTPRequestHandler({
|
|
|
|
|
router: appRouter,
|
|
|
|
|
createContext: () => ({ conn: null }),
|
|
|
|
|
req,
|
|
|
|
|
res,
|
|
|
|
|
path: procPath,
|
|
|
|
|
}).catch((err: unknown) => {
|
|
|
|
|
logger.error({ err }, "tRPC HTTP handler failed");
|
|
|
|
|
if (!res.headersSent) res.status(500).json({ error: "INTERNAL" });
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-06-01 21:44:29 +07:00
|
|
|
|
|
|
|
|
// 404 handler
|
|
|
|
|
app.use((_req: Request, res: Response) => {
|
|
|
|
|
res.status(404).json({
|
|
|
|
|
error: "NOT_FOUND",
|
2026-08-16 13:25:10 +07:00
|
|
|
message:
|
|
|
|
|
"Endpoint not found — data APIs are served over /trpc (WebSocket/HTTP)",
|
2026-06-01 21:44:29 +07:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Error handler (must be last)
|
|
|
|
|
app.use(errorHandler);
|
|
|
|
|
|
|
|
|
|
return app;
|
|
|
|
|
}
|