Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@mozilla/readability": "^0.6.0",
"@openrouter/ai-sdk-provider": "^1.2.5",
"@orpc/client": "^1.11.3",
"@orpc/openapi": "^1.12.2",
"@orpc/server": "^1.11.3",
"@orpc/zod": "^1.11.3",
"@radix-ui/react-checkbox": "^1.3.3",
Expand Down
4 changes: 2 additions & 2 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ module.exports = {
transform: {
"^.+\\.(ts|tsx|js|mjs)$": ["babel-jest"],
},
// Transform ESM modules (like shiki, @orpc) to CommonJS for Jest
transformIgnorePatterns: ["node_modules/(?!(@orpc|shiki)/)"],
// Transform ESM modules to CommonJS for Jest
transformIgnorePatterns: ["node_modules/(?!(@orpc|shiki|json-schema-typed|rou3)/)"],
// Run tests in parallel (use 50% of available cores, or 4 minimum)
maxWorkers: "50%",
// Force exit after tests complete to avoid hanging on lingering handles
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"@mozilla/readability": "^0.6.0",
"@openrouter/ai-sdk-provider": "^1.2.5",
"@orpc/client": "^1.11.3",
"@orpc/openapi": "^1.12.2",
"@orpc/server": "^1.11.3",
"@orpc/zod": "^1.11.3",
"@radix-ui/react-checkbox": "^1.3.3",
Expand Down
80 changes: 80 additions & 0 deletions src/node/orpc/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import { WebSocketServer } from "ws";
import { RPCHandler } from "@orpc/server/node";
import { RPCHandler as ORPCWebSocketServerHandler } from "@orpc/server/ws";
import { onError } from "@orpc/server";
import { OpenAPIGenerator } from "@orpc/openapi";
import { OpenAPIHandler } from "@orpc/openapi/node";
import { ZodToJsonSchemaConverter } from "@orpc/zod/zod4";
import { router, type AppRouter } from "@/node/orpc/router";
import type { ORPCContext } from "@/node/orpc/context";
import { extractWsHeaders } from "@/node/orpc/authMiddleware";
Expand Down Expand Up @@ -53,6 +56,10 @@ export interface OrpcServer {
baseUrl: string;
/** WebSocket URL for WS connections */
wsUrl: string;
/** URL for OpenAPI spec JSON */
specUrl: string;
/** URL for Scalar API docs */
docsUrl: string;
/** Close the server and cleanup resources */
close: () => Promise<void>;
}
Expand Down Expand Up @@ -100,6 +107,77 @@ export async function createOrpcServer({

const orpcRouter = existingRouter ?? router(authToken);

// OpenAPI generator for spec endpoint
const openAPIGenerator = new OpenAPIGenerator({
schemaConverters: [new ZodToJsonSchemaConverter()],
});

// OpenAPI spec endpoint
app.get("/api/spec.json", async (_req, res) => {
const spec = await openAPIGenerator.generate(orpcRouter, {
info: {
title: "Mux API",
version: VERSION.git_describe,
description: "API for Mux",
},
servers: [{ url: "/api" }],
security: authToken ? [{ bearerAuth: [] }] : undefined,
components: authToken
? {
securitySchemes: {
bearerAuth: {
type: "http",
scheme: "bearer",
},
},
}
: undefined,
});
res.json(spec);
});

// Scalar API reference UI
app.get("/api/docs", (_req, res) => {
const html = `<!doctype html>
<html>
<head>
<title>mux API Reference</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
<script>
Scalar.createApiReference('#app', {
url: '/api/spec.json',
${authToken ? "authentication: { securitySchemes: { bearerAuth: { token: '' } } }," : ""}
})
</script>
</body>
</html>`;
res.setHeader("Content-Type", "text/html");
res.send(html);
});

// OpenAPI REST handler (for Scalar/OpenAPI clients)
const openAPIHandler = new OpenAPIHandler(orpcRouter, {
interceptors: [onError(onOrpcError)],
});

app.use("/api", async (req, res, next) => {
// Skip spec.json and docs routes - they're handled above
if (req.path === "/spec.json" || req.path === "/docs") {
return next();
}
const { matched } = await openAPIHandler.handle(req, res, {
prefix: "/api",
context: { ...context, headers: req.headers },
});
if (matched) return;
next();
});

// oRPC HTTP handler
const orpcHandler = new RPCHandler(orpcRouter, {
interceptors: [onError(onOrpcError)],
Expand Down Expand Up @@ -161,6 +239,8 @@ export async function createOrpcServer({
port: actualPort,
baseUrl: `http://${connectableHost}:${actualPort}`,
wsUrl: `ws://${connectableHost}:${actualPort}/orpc/ws`,
specUrl: `http://${connectableHost}:${actualPort}/api/spec.json`,
docsUrl: `http://${connectableHost}:${actualPort}/api/docs`,
close: async () => {
// Close WebSocket server first
wsServer.close();
Expand Down