diff --git a/apps/cli/src/commands/core.ts b/apps/cli/src/commands/core.ts index 411388038..4d0a33ac1 100644 --- a/apps/cli/src/commands/core.ts +++ b/apps/cli/src/commands/core.ts @@ -19,7 +19,6 @@ import { registerGetCommand } from "./core/get"; import { registerLoginCommand } from "./core/login"; import { registerRemoveCommand } from "./core/remove"; import { registerSignupCommand } from "./core/signup"; -import { registerStatusCommand } from "./core/status"; import { registerStudioCommand } from "./core/studio"; import { registerUpdateCommand } from "./core/update"; @@ -121,7 +120,6 @@ export function registerCoreCommands(program: DirectorCommand): void { }); registerConfigCommand(program); - registerStatusCommand(program); registerDebugCommands(program); } diff --git a/apps/cli/src/commands/core/get.ts b/apps/cli/src/commands/core/get.ts index 6fcba0c76..01ea84d4b 100644 --- a/apps/cli/src/commands/core/get.ts +++ b/apps/cli/src/commands/core/get.ts @@ -1,6 +1,4 @@ import type { GatewayRouterOutputs } from "@director.run/gateway/client"; -import { getSSEPathForPlaybook } from "@director.run/gateway/helpers"; -import { getStreamablePathForPlaybook } from "@director.run/gateway/helpers"; import { blue, green, @@ -111,11 +109,8 @@ export function printPlaybookDetails( id, name, description: description ?? "--", - streamableURL: joinURL( - env.GATEWAY_URL, - getStreamablePathForPlaybook(playbook.id), - ), - sseURL: joinURL(env.GATEWAY_URL, getSSEPathForPlaybook(playbook.id)), + streamableURL: joinURL(env.GATEWAY_URL, playbook.paths.streamable), + sseURL: joinURL(env.GATEWAY_URL, playbook.paths.sse), }), ); diff --git a/apps/cli/src/commands/core/status.ts b/apps/cli/src/commands/core/status.ts deleted file mode 100644 index a364f637e..000000000 --- a/apps/cli/src/commands/core/status.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { getStatus } from "@director.run/gateway/status"; -import { DirectorCommand } from "@director.run/utilities/cli/director-command"; -import { actionWithErrorHandler } from "@director.run/utilities/cli/index"; -import cliPackage from "../../../package.json"; - -export function registerStatusCommand(program: DirectorCommand) { - program - .command("status") - .description("Get the status of the director") - .action( - actionWithErrorHandler(async () => { - const status = await getStatus(cliPackage.version); - console.log(status); - }), - ); -} diff --git a/apps/cli/src/integration.test.ts b/apps/cli/src/integration.test.ts index 09ceaf148..f4bda52e3 100644 --- a/apps/cli/src/integration.test.ts +++ b/apps/cli/src/integration.test.ts @@ -29,7 +29,6 @@ describe("CLI integration tests", () => { gateway = await Gateway.start({ database, - baseUrl: baseURL, port: env.PORT, }); diff --git a/apps/gateway/bin/server.ts b/apps/gateway/bin/server.ts index f2b93396e..157d92bf7 100644 --- a/apps/gateway/bin/server.ts +++ b/apps/gateway/bin/server.ts @@ -22,7 +22,6 @@ async function start() { await Gateway.start({ database, - baseUrl: env.BASE_URL, port: env.PORT, studioAssetsPath: getStudioAssetsPath(), }); diff --git a/apps/gateway/src/gateway.ts b/apps/gateway/src/gateway.ts index 000f21741..bec5089f7 100644 --- a/apps/gateway/src/gateway.ts +++ b/apps/gateway/src/gateway.ts @@ -1,5 +1,4 @@ import { Server } from "node:http"; -import { createOauthCallbackRouter } from "@director.run/mcp/oauth/oauth-callback-router"; import { isDevelopment } from "@director.run/utilities/env"; import { getLogger } from "@director.run/utilities/logger"; import { @@ -7,9 +6,7 @@ import { notFoundHandler, } from "@director.run/utilities/middleware/index"; import { logRequests } from "@director.run/utilities/middleware/index"; -import { spaMiddleware } from "@director.run/utilities/middleware/spa"; import { Telemetry } from "@director.run/utilities/telemetry"; -import { joinURL } from "@director.run/utilities/url"; import { toNodeHandler } from "better-auth/node"; import cors from "cors"; import express from "express"; @@ -18,6 +15,8 @@ import type { Database } from "./db/database"; import { env } from "./env"; import { PlaybookStore } from "./playbooks/playbook-store"; import { createMCPRouter } from "./routers/mcp/mcp"; +import { createOauthClientRouter } from "./routers/oauth-client-callback"; +import { createStudioRouter } from "./routers/studio"; import { createTRPCExpressMiddleware } from "./routers/trpc"; const logger = getLogger("Gateway"); @@ -28,7 +27,6 @@ export class Gateway { public readonly database: Database; private app: express.Express; private studioAssetsPath?: string; - private baseUrl: string; public readonly port: number; private constructor(attribs: { @@ -36,13 +34,11 @@ export class Gateway { database: Database; telemetry?: Telemetry; studioAssetsPath?: string; - baseUrl: string; port: number; }) { this.playbookStore = attribs.playbookStore; this.database = attribs.database; this.studioAssetsPath = attribs.studioAssetsPath; - this.baseUrl = attribs.baseUrl; this.port = attribs.port; this.app = express(); @@ -53,32 +49,12 @@ export class Gateway { credentials: true, }), ); - this.app.use(logRequests()); - if (this.studioAssetsPath) { - logger.debug({ - message: "serving studio assets from", - distPath: this.studioAssetsPath, - }); - this.app.use( - "/studio", - spaMiddleware({ - distPath: this.studioAssetsPath, - config: { - basePath: "/studio", - gatewayUrl: this.baseUrl, - registryUrl: env.REGISTRY_URL, - }, - }), - ); - this.app.get("/", (_, res) => { - res.redirect("/studio"); - }); - } else { - logger.warn({ - message: "studioAssetsPath not provided, studio will not be available", - }); - } + this.app.use(logRequests()); + this.app.use( + "/", + createStudioRouter({ assetsPath: this.studioAssetsPath }), + ); this.app.use( "/playbooks", @@ -90,61 +66,7 @@ export class Gateway { this.app.use( "/", - createOauthCallbackRouter({ - getSession: async (req) => { - const session = await auth.api.getSession({ - headers: req.headers as Record, - }); - if (!session) { - return null; - } - return { userId: session.user.id }; - }, - onAuthorizationSuccess: async (factoryId, providerId, code, userId) => { - await this.playbookStore.onAuthorizationSuccess( - factoryId, - providerId, - code, - userId, - ); - if (this.studioAssetsPath) { - // Redirect to hosted studio callback page - return { - redirectUrl: joinURL( - this.baseUrl, - `studio/oauth/${factoryId}/${providerId}/callback`, - ), - }; - } else if (isDevelopment()) { - // redirect to dev studio callback page - return { - redirectUrl: `http://localhost:3000/oauth/${factoryId}/${providerId}/callback`, - }; - } - }, - onAuthorizationError: (factoryId, providerId, error) => { - logger.error({ - error, - message: `failed to authorize ${factoryId} ${providerId}: ${error.message}`, - }); - // Only expose the error message to the client, not stack traces or internal details - const safeErrorMessage = encodeURIComponent(error.message); - if (this.studioAssetsPath) { - // Redirect to hosted studio callback page - return { - redirectUrl: joinURL( - this.baseUrl, - `studio/oauth/${factoryId}/${providerId}/callback?error=${safeErrorMessage}`, - ), - }; - } else if (isDevelopment()) { - // redirect to dev studio callback page - return { - redirectUrl: `http://localhost:3000/oauth/${factoryId}/${providerId}/callback?error=${safeErrorMessage}`, - }; - } - }, - }), + createOauthClientRouter({ playbookStore: this.playbookStore }), ); // SECURITY: Force consent screen for MCP OAuth authorize requests @@ -203,13 +125,7 @@ export class Gateway { database: this.database, }), ); - this.app.get("/", (_, res, next) => { - if (this.studioAssetsPath) { - res.redirect("/studio"); - } else { - return next(); - } - }); + this.app.all("*", notFoundHandler); this.app.use(errorRequestHandler); } @@ -219,7 +135,6 @@ export class Gateway { studioAssetsPath?: string; database: Database; telemetry?: Telemetry; - baseUrl: string; port: number; }, successCallback?: () => void, @@ -228,7 +143,7 @@ export class Gateway { const playbookStore = await PlaybookStore.create({ database: attribs.database, telemetry: attribs.telemetry, - baseCallbackUrl: attribs.baseUrl, + baseCallbackUrl: env.BASE_URL, }); attribs.telemetry?.trackEvent("gateway_start"); @@ -238,7 +153,6 @@ export class Gateway { playbookStore, telemetry: attribs.telemetry, studioAssetsPath: attribs.studioAssetsPath, - baseUrl: attribs.baseUrl, port: attribs.port, }); diff --git a/apps/gateway/src/helpers.ts b/apps/gateway/src/helpers.ts deleted file mode 100644 index 67a823631..000000000 --- a/apps/gateway/src/helpers.ts +++ /dev/null @@ -1,7 +0,0 @@ -export function getStreamablePathForPlaybook(playbookId: string) { - return `/playbooks/${playbookId}/mcp`; -} - -export function getSSEPathForPlaybook(playbookId: string) { - return `/playbooks/${playbookId}/sse`; -} diff --git a/apps/gateway/src/playbooks/playbook.ts b/apps/gateway/src/playbooks/playbook.ts index 7fc6d1516..a0e2d36a6 100644 --- a/apps/gateway/src/playbooks/playbook.ts +++ b/apps/gateway/src/playbooks/playbook.ts @@ -14,10 +14,6 @@ import { PromptManager, } from "../capabilities/prompt-manager"; import type { Database } from "../db/database"; -import { - getSSEPathForPlaybook, - getStreamablePathForPlaybook, -} from "../helpers"; import { type PlaybookHTTPTarget, PlaybookHTTPTargetSchema, @@ -94,6 +90,14 @@ export class Playbook extends ProxyServer { return this._userId; } + get streamablePath() { + return `/playbooks/${this.id}/mcp`; + } + + get ssePath() { + return `/playbooks/${this.id}/sse`; + } + public async addTarget( server: PlaybookTarget | ProxyTarget, params: { throwOnError: boolean } = { throwOnError: true }, @@ -328,8 +332,8 @@ export class Playbook extends ProxyServer { ), ), paths: { - streamable: getStreamablePathForPlaybook(this.id), - sse: getSSEPathForPlaybook(this.id), + streamable: this.streamablePath, + sse: this.ssePath, }, }; } diff --git a/apps/gateway/src/routers/oauth-client-callback.ts b/apps/gateway/src/routers/oauth-client-callback.ts new file mode 100644 index 000000000..d753f6a8d --- /dev/null +++ b/apps/gateway/src/routers/oauth-client-callback.ts @@ -0,0 +1,51 @@ +import { createOauthCallbackRouter } from "@director.run/mcp/oauth/oauth-callback-router"; +import { getLogger } from "@director.run/utilities/logger"; +import {} from "@director.run/utilities/middleware/index"; +import { joinURL } from "@director.run/utilities/url"; +import { auth } from "../auth"; +import { env } from "../env"; +import { PlaybookStore } from "../playbooks/playbook-store"; + +const logger = getLogger("OauthClientRouter"); + +export const createOauthClientRouter = ({ + playbookStore, +}: { playbookStore: PlaybookStore }) => + createOauthCallbackRouter({ + getSession: async (req) => { + const session = await auth.api.getSession({ + headers: req.headers as Record, + }); + if (!session) { + return null; + } + return { userId: session.user.id }; + }, + onAuthorizationSuccess: async (factoryId, providerId, code, userId) => { + await playbookStore.onAuthorizationSuccess( + factoryId, + providerId, + code, + userId, + ); + return { + redirectUrl: joinURL( + env.BASE_URL, + `studio/oauth/${factoryId}/${providerId}/callback`, + ), + }; + }, + onAuthorizationError: (factoryId, providerId, error) => { + logger.error({ + error, + message: `failed to authorize ${factoryId} ${providerId}: ${error.message}`, + }); + + return { + redirectUrl: joinURL( + env.BASE_URL, + `studio/oauth/${factoryId}/${providerId}/callback?error=${encodeURIComponent(error.message)}`, + ), + }; + }, + }); diff --git a/apps/gateway/src/routers/studio.ts b/apps/gateway/src/routers/studio.ts new file mode 100644 index 000000000..fc6a23da0 --- /dev/null +++ b/apps/gateway/src/routers/studio.ts @@ -0,0 +1,42 @@ +import { isDevelopment } from "@director.run/utilities/env"; +import { getLogger } from "@director.run/utilities/logger"; +import { spaMiddleware } from "@director.run/utilities/middleware/spa"; +import express from "express"; +import { env } from "../env"; + +const logger = getLogger("studio_router"); + +export function createStudioRouter({ + assetsPath, +}: { + assetsPath?: string; +}): express.Router { + const router = express.Router(); + if (assetsPath) { + logger.debug({ + message: "serving studio assets from", + distPath: assetsPath, + }); + router.use( + "/studio", + spaMiddleware({ + distPath: assetsPath, + config: { + basePath: "/studio", + gatewayUrl: env.BASE_URL, + registryUrl: env.REGISTRY_URL, + }, + }), + ); + + router.get("/", (_, res) => { + res.redirect("/studio"); + }); + } else if (isDevelopment()) { + router.use("/studio", (req, res) => { + res.redirect(`http://localhost:3000${req.originalUrl}`); + }); + } + + return router; +} diff --git a/apps/gateway/src/routers/trpc/index.ts b/apps/gateway/src/routers/trpc/index.ts index 2e96a5b13..416ffd2d7 100644 --- a/apps/gateway/src/routers/trpc/index.ts +++ b/apps/gateway/src/routers/trpc/index.ts @@ -6,14 +6,12 @@ import type { Database } from "../../db/database"; import type { UserStatus } from "../../db/schema"; import { env } from "../../env"; import { PlaybookStore } from "../../playbooks/playbook-store"; -import { getStatus } from "../../status"; import { createAuthRouter } from "./auth-router"; import { createSettingsRouter } from "./settings-router"; import { createPlaybookStoreRouter } from "./store-router"; import { createToolsRouter } from "./tools-router"; export type GatewayContext = { - cliVersion: string | null; playbookStore: PlaybookStore; database: Database; userId: string | undefined; @@ -50,8 +48,8 @@ export const protectedProcedure = publicProcedure.use(enforceUserIsAuthed); export function createAppRouter() { return t.router({ - health: publicProcedure.query(({ ctx }) => { - return getStatus(ctx.cliVersion); + health: publicProcedure.query(() => { + return { status: "ok" }; }), auth: createAuthRouter(), store: createPlaybookStoreRouter(), @@ -69,10 +67,7 @@ export function createTRPCExpressMiddleware({ }): ReturnType { return trpcExpress.createExpressMiddleware({ router: createAppRouter(), - createContext: async ({ req, res }): Promise => { - const headerValue = res.getHeader("x-cli-version"); - const cliVersion = typeof headerValue === "string" ? headerValue : null; - + createContext: async ({ req }): Promise => { let userId: string | undefined = undefined; let userStatus: UserStatus | undefined = undefined; @@ -87,7 +82,6 @@ export function createTRPCExpressMiddleware({ } return { - cliVersion, playbookStore, database, userId, diff --git a/apps/gateway/src/routers/trpc/store-router.ts b/apps/gateway/src/routers/trpc/store-router.ts index 29c43620c..fa7d8de21 100644 --- a/apps/gateway/src/routers/trpc/store-router.ts +++ b/apps/gateway/src/routers/trpc/store-router.ts @@ -10,10 +10,6 @@ import { joinURL } from "@director.run/utilities/url"; import { z } from "zod"; import { auth } from "../../auth"; import { env } from "../../env"; -import { - getSSEPathForPlaybook, - getStreamablePathForPlaybook, -} from "../../helpers"; import type { PlaybookTarget } from "../../playbooks/playbook"; import { type AuthenticatedGatewayContext, protectedProcedure } from "./index"; @@ -586,11 +582,8 @@ export function createPlaybookStoreRouter() { } // Build URLs without API key (key returned separately for obfuscation) - const streamablePath = getStreamablePathForPlaybook(playbookId); - const ssePath = getSSEPathForPlaybook(playbookId); - - const streamableUrl = joinURL(env.BASE_URL, streamablePath); - const sseUrl = joinURL(env.BASE_URL, ssePath); + const streamableUrl = joinURL(env.BASE_URL, playbook.streamablePath); + const sseUrl = joinURL(env.BASE_URL, playbook.ssePath); return { playbookId, diff --git a/apps/gateway/src/status.ts b/apps/gateway/src/status.ts deleted file mode 100644 index 2f97e06ef..000000000 --- a/apps/gateway/src/status.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { isCommandInPath } from "@director.run/utilities/os"; - -export function getStatus(cliVersion: string | null) { - return { - platform: process.platform, - dependencies: [ - { - name: "npx", - installed: isCommandInPath("npx"), - }, - { - name: "uvx", - installed: isCommandInPath("uvx"), - }, - ], - cliVersion, - }; -} diff --git a/apps/gateway/src/test/integration.ts b/apps/gateway/src/test/integration.ts index 5fc7a156b..c8ee11c1e 100644 --- a/apps/gateway/src/test/integration.ts +++ b/apps/gateway/src/test/integration.ts @@ -158,7 +158,6 @@ export class IntegrationTestHarness { const gateway = await Gateway.start({ database, - baseUrl: baseURL, port: env.PORT, });