import { createStartHandler, defaultStreamHandler, } from "@tanstack/react-start/server"; import { routeAgentRequest } from "agents"; import { resolveUserContextFromHeaders } from "@/middleware/ensure-user/resolve"; import { ProjectRepository } from "@/server/features/projects/repositories/ProjectRepository"; import { SamSessionRepository } from "@/server/features/sam/SamSessionRepository"; import { runScheduledRankChecks } from "@/server/features/rank-tracking/services/scheduledRankChecks"; import { getOrCreateOrganizationCustomer } from "@/server/billing/subscription"; import { isHostedServerAuthMode } from "@/server/lib/runtime-env"; import { getAuthMode, isHostedAuthMode } from "@/lib/auth-mode"; import { createOpenSeoOAuthProvider, type OpenSeoOAuthEnv, } from "@/server/mcp/oauth-provider"; import { requestWithPublicOrigin } from "@/server/mcp/public-origin"; import { MCP_ROUTE } from "@/server/mcp/context"; import { handleSelfHostedOpenSeoMcpRequest } from "@/server/mcp/transport"; import { withPgClient } from "@/db"; import { AUTUMN_WEBHOOK_PATH, handleAutumnWebhookRequest, } from "@/server/billing/autumn-webhook"; import { maybeSendSelfHostHeartbeat } from "@/server/lib/self-host-telemetry"; const appFetch = createStartHandler(defaultStreamHandler); const openSeoOAuthProvider = createOpenSeoOAuthProvider(appFetch); // Authorize an onboarding-chat connection in the Worker, before it reaches the // Durable Object. The DO instance name is the projectId (set client-side); we // resolve the session here and confirm the caller's org owns that project, so // the DO can trust its `name`. Returning a Response rejects; void lets it through. async function authorizeOnboardingChat( request: Request, projectId: string, ): Promise { let context; try { context = await resolveUserContextFromHeaders(request.headers); } catch { return new Response("Unauthorized", { status: 401 }); } const project = await ProjectRepository.getProjectForOrganization( projectId, context.organizationId, ); if (!project) { return new Response("Forbidden", { status: 403 }); } // Ensure the org's Autumn customer exists (and gets its default onboarding-plan // credits) before the DO checks the balance — otherwise a brand-new org's first // message can hit a false "out of credits" gate. Hosted-only; self-hosted has // no Autumn. if (await isHostedServerAuthMode()) { await getOrCreateOrganizationCustomer(context); } return undefined; } // Authorize a SAM agent connection in the Worker, before it reaches the Durable // Object. The DO instance name is the sessionId (set client-side); we resolve // the session here and authorize the caller against the session's project via // the same canonical project-access check the rest of the app uses, so the DO // can trust its `name` and derive org/project/user from the session row. async function authorizeSamChat( request: Request, sessionId: string, ): Promise { let context; try { context = await resolveUserContextFromHeaders(request.headers); } catch { return new Response("Unauthorized", { status: 401 }); } const session = await SamSessionRepository.getActiveSession( sessionId, context.userId, ); const project = session ? await ProjectRepository.getProjectForOrganization( session.projectId, context.organizationId, ) : null; if (!session || !project) { return new Response("Forbidden", { status: 403 }); } // Same as onboarding above: make sure the Autumn customer (and its default // free-plan credits) exists before the DO's balance gate runs, or a brand-new // org's first message hits a false "out of credits". if (await isHostedServerAuthMode()) { await getOrCreateOrganizationCustomer(context); } return undefined; } // Both chat DOs live behind /agents/*. Dispatch on the DO binding partyserver // resolved for the request (rather than re-parsing the path), and fail closed // on anything unrecognized. function authorizeChatAgent( request: Request, lobby: { className: string; name: string }, ): Promise | Response { switch (lobby.className) { case "SAM_CHAT": return authorizeSamChat(request, lobby.name); case "ONBOARDING_CHAT": return authorizeOnboardingChat(request, lobby.name); default: return new Response("Forbidden", { status: 403 }); } } // Route /agents/* to the onboarding and SAM chat DOs. Auth happens here (both // the WS upgrade and any HTTP message-history fetch), keeping it off the OAuth // wrapper and TanStack route guard below. async function routeChatAgents(request: Request, env: Env): Promise { const response = await routeAgentRequest(request, env, { onBeforeConnect: (req, lobby) => authorizeChatAgent(req, lobby), onBeforeRequest: (req, lobby) => authorizeChatAgent(req, lobby), }); return response ?? new Response("Not found", { status: 404 }); } function fetch( request: Request, env: Env, ctx: ExecutionContext, ): Promise { // Scope a per-request Postgres client (no-op in D1 mode). The client isn't // closed here — the Workers↔Hyperdrive socket is reclaimed at invocation end. return withPgClient(() => Promise.resolve(handleFetch(request, env, ctx))); } function handleFetch( request: Request, env: Env, ctx: ExecutionContext, ): Response | Promise { ctx.waitUntil(maybeSendSelfHostHeartbeat()); const authMode = getAuthMode(env.AUTH_MODE); const publicRequest = requestWithPublicOrigin(request); const pathname = new URL(publicRequest.url).pathname; if (pathname.startsWith("/agents/")) { return routeChatAgents(publicRequest, env); } if (isHostedAuthMode(authMode)) { if (pathname === AUTUMN_WEBHOOK_PATH) { return handleAutumnWebhookRequest(publicRequest); } return openSeoOAuthProvider.fetch( publicRequest, env as OpenSeoOAuthEnv, ctx, ); } if ( (authMode === "cloudflare_access" || authMode === "local_noauth") && pathname === MCP_ROUTE ) { return handleSelfHostedOpenSeoMcpRequest(publicRequest, authMode, env, ctx); } return appFetch(request); } // Export Workflow classes as named exports export { SiteAuditWorkflow } from "./server/workflows/SiteAuditWorkflow"; export { RankCheckWorkflow } from "./server/workflows/RankCheckWorkflow"; // Durable Object class for the onboarding strategy chat (Agents SDK). export { OnboardingChatAgent } from "./server/features/onboarding/OnboardingChatAgent"; // Durable Object class for the SAM in-app agent (Agents SDK). export { SamChatAgent } from "./server/features/sam/SamChatAgent"; export default { fetch, async scheduled( _controller: ScheduledController, env: Env, _ctx: ExecutionContext, ) { // Scope a per-request Postgres client for the cron run (no-op in D1 mode). await withPgClient(() => runScheduledRankChecks(env)); }, };