import { randomUUID } from "node:crypto"; import { rm } from "node:fs/promises"; import { ORPCError, onError } from "@orpc/server"; import { RPCHandler } from "@orpc/server/fetch"; import type { AgentRuntime, JobPublisher, ManagedConnectorProvider, MessagingSurface, RealtimeFanout, SandboxProvider, TransactionalEmailProvider, } from "@rakazo/adapter-kit"; import type { ComposioProvider, ConnectorRegistry, DestinationEmulator, RemoteConnectorDependencies, } from "@rakazo/adapters"; import { applyMessagingOutboundStatus, ChatSdkMessagingSurface, ComposioConnector, createBackgroundJobHandlers, createCloudAgentConnection, createConnectorStack, createJobReconciler, createMessagingContextLoader, createMessagingTeamChatSender, createRunExecutor, createRunSandbox, createRunSecretWriter, createWebProvider, destroyBot, EmailEmulator, EncryptedSecretStore, ExpoPushProvider, GraphileJobPublisher, InMemoryJobQueue, InMemoryRealtimeFanout, InstalledConnectorProvider, IntegrationProviderSettings, isComposioEnabled, isMessagingSurfaceEnabled, isPipedreamEnabled, LocalAgentHomeStore, LocalArtifactStore, McpConnector, McpOAuthBroker, messagingPlatformsFromEnv, PiAgentRuntime, PiOAuthLogins, PipedreamConnector, PostgresRealtimeFanout, pipedreamConfigFromEnv, piSessionsRoot, pushTokenPath, reconcileCloudAgents, reconcileComputerUpdates, removePiUserSessions, ScriptedAgentRuntime, SmtpEmailProvider, SpaceMemoryProviderResolver, toTeamChatInbound, } from "@rakazo/adapters"; import { blockedAuthPaths, createAuth } from "@rakazo/auth"; import { signupPolicyFromEnv } from "@rakazo/core"; import type { Pool, PrismaClient } from "@rakazo/db"; import { createDb, createPool, createThreadEvents, parsePositiveInteger, provisionMessagingIdentity, requireMembership, } from "@rakazo/db"; import type { Logger } from "@rakazo/logging"; import { createServiceLogger, enrichLogContext, getLogger, installLogger, SERVICE_NAMES, } from "@rakazo/logging"; import { requestLogging } from "@rakazo/logging/hono"; import { MarkdownMemoryStore } from "@rakazo/memory"; import { Hono } from "hono"; import { cors } from "hono/cors"; import type { AppEnv } from "./env.js"; import { loadEnv } from "./env.js"; import { mountLocalSettings } from "./local-settings.js"; import { createMessagingInboundHandler, teamChatSenderCanWakeMessageRoutines, wakeMessageRoutines, } from "./messaging-inbound.js"; import { mountMessagingWebhookRoutes } from "./messaging-webhook.js"; import { mountApiRequestBodyLimits } from "./request-body-limit.js"; import { createRouter } from "./router.js"; import { mountScreenTarget } from "./screen-proxy.js"; import { isDeferredReservationLost, TeamChatBridge } from "./team-chat-bridge.js"; import { ModelTeamChatEngagementJudge } from "./team-chat-judge.js"; import { PendingTeamChatInbound, prefersTeamChatSurface, settleWithTimeout, TEAM_CHAT_STARTUP_SHUTDOWN_MS, } from "./team-chat-startup.js"; import { mountVoiceHttpRoutes } from "./voice.js"; import { mountWebhookHttpRoutes } from "./webhook.js"; export interface AppHandles { app: Hono; prisma: PrismaClient; jobs: JobPublisher; sandbox: SandboxProvider; connector: DestinationEmulator; composio?: ComposioProvider; connectors: ConnectorRegistry; messaging?: MessagingSurface; email?: TransactionalEmailProvider; executor: ReturnType; runtime: AgentRuntime; stop: () => Promise; } export async function createApp( overrides: Partial & { prisma?: PrismaClient; realtime?: RealtimeFanout; sandbox?: SandboxProvider; composio?: ComposioProvider; pipedream?: ManagedConnectorProvider; messaging?: MessagingSurface; email?: TransactionalEmailProvider; remoteConnectors?: RemoteConnectorDependencies; logger?: Logger; } = {}, ): Promise { const { prisma: prismaOverride, realtime: realtimeOverride, sandbox: sandboxOverride, composio: composioOverride, pipedream: pipedreamOverride, messaging: messagingOverride, email: emailOverride, remoteConnectors, logger: loggerOverride, ...envOverrides } = overrides; const env = { ...loadEnv(process.env), ...envOverrides }; const logger = loggerOverride ?? createServiceLogger({ service: SERVICE_NAMES.api }); installLogger(logger); const created = prismaOverride ? { prisma: prismaOverride, pool: undefined } : createDb(env.databaseUrl, { poolMax: parsePositiveInteger(process.env.DB_POOL_MAX, 4), applicationName: "rakazo-api", }); const { prisma } = created; const realtime = realtimeOverride ?? (created.pool ? new PostgresRealtimeFanout({ connectionString: env.realtimeDatabaseUrl, publisher: created.pool, }) : new InMemoryRealtimeFanout()); const secrets = new EncryptedSecretStore(env.encryptionKey); const events = createThreadEvents(prisma, realtime, { runSecretWriter: createRunSecretWriter(secrets), }); const environmentSignupPolicy = signupPolicyFromEnv(env); const deploymentSettings = await prisma.deploymentSettings.upsert({ where: { id: "default" }, create: { id: "default", signupsEnabled: environmentSignupPolicy.enabled, signupAllowlist: environmentSignupPolicy.allowlist.join(","), signupPolicyInitialized: true, }, update: {}, }); if (!deploymentSettings.signupPolicyInitialized) { // Older versions created this row with schema defaults even though auth // still enforced the environment policy. Copy that effective policy once // so upgrades preserve behavior before Settings becomes authoritative. await prisma.deploymentSettings.updateMany({ where: { id: "default", signupPolicyInitialized: false }, data: { signupsEnabled: environmentSignupPolicy.enabled, signupAllowlist: environmentSignupPolicy.allowlist.join(","), signupPolicyInitialized: true, }, }); } const jobKind = env.wakeupDriver; const inMemoryJobs = jobKind === "memory" ? new InMemoryJobQueue() : undefined; // prismaOverride skips createDb, so there is no shared pool. The previous // GraphileJobPublisher(databaseUrl) path opened its own connections; keep a // bounded pool for that override path instead of passing undefined. let ownedJobPool: Pool | undefined; if (!inMemoryJobs && !created.pool) { ownedJobPool = createPool(env.databaseUrl, { poolMax: parsePositiveInteger(process.env.DB_POOL_MAX, 4), applicationName: "rakazo-api-jobs", }); } const jobPool = created.pool ?? ownedJobPool; const jobs = inMemoryJobs ? inMemoryJobs : new GraphileJobPublisher( jobPool ?? (() => { throw new Error("Graphile job publisher requires a PostgreSQL pool"); })(), ); const sandbox: SandboxProvider = sandboxOverride ?? createRunSandbox(env.sandboxProvider, { supervisorUrl: env.sandboxSupervisorUrl, supervisorToken: env.sandboxSupervisorToken, e2bApiKey: env.e2bApiKey, daytonaApiKey: env.daytonaApiKey, daytonaApiUrl: env.daytonaApiUrl, daytonaTarget: env.daytonaTarget, boxApiKey: env.boxApiKey, boxApiUrl: env.boxApiUrl, dataDir: env.dataDir, prisma, }); const mcpOAuth = new McpOAuthBroker(prisma, secrets, remoteConnectors); const memoryProviders = new SpaceMemoryProviderResolver(prisma, secrets); const oauthLogins = new PiOAuthLogins(); const home = new LocalAgentHomeStore(env.dataDir); const artifacts = new LocalArtifactStore(env.dataDir); const memory = new MarkdownMemoryStore(prisma); const mcp = new McpConnector( prisma, secrets, { stdioEnabled: env.mcpStdioEnabled, allowedCommands: env.mcpStdioAllowedCommands, network: remoteConnectors, }, mcpOAuth, ); const pipedreamConfig = pipedreamConfigFromEnv(env); const pipedream = pipedreamOverride ?? (isPipedreamEnabled(pipedreamConfig) ? new PipedreamConnector(pipedreamConfig) : undefined); // This process registers the inbound sink (messaging.onInbound below), // so it's the one that must hold Telegram's live getUpdates connection — // see messagingPlatformsFromEnv's docstring for why a second poller // elsewhere (e.g. the worker) would actively break this. const messagingPlatforms = messagingPlatformsFromEnv(env, { pollInboundMessages: true }); const messaging = messagingOverride ?? (isMessagingSurfaceEnabled(messagingPlatforms, { deploymentModelKey: env.deploymentModelKey, openSignup: env.messagingOpenSignup, }) ? new ChatSdkMessagingSurface(messagingPlatforms) : undefined); const localEmailEmulator = !emailOverride && !env.smtpUrl && env.emailEmulator ? new EmailEmulator((message) => { getLogger().info("email emulator captured message", { "email.subject": message.subject, }); }) : undefined; if (localEmailEmulator && !isLoopbackHost(env.apiHost)) { throw new Error("EMAIL_EMULATOR requires API_HOST to be a loopback host"); } const email: TransactionalEmailProvider | undefined = emailOverride ?? (env.smtpUrl ? new SmtpEmailProvider({ url: env.smtpUrl, from: env.emailFrom ?? "" }) : localEmailEmulator); const installed = new InstalledConnectorProvider(prisma, secrets, remoteConnectors); const integrationSettings = new IntegrationProviderSettings(prisma, secrets, env.encryptionKey, { composio: composioOverride ?? (isComposioEnabled(env.composioApiKey) ? new ComposioConnector(env.composioApiKey) : undefined), pipedream, }); const stack = createConnectorStack(false, composioOverride, [ installed, ...integrationSettings .providers() .filter((provider) => !composioOverride || provider.describe().id !== "composio"), mcp, ]); const connector = stack.destination; await connector.start(); integrationSettings.warmDirectories(); const runtime = env.agentRuntime === "scripted" ? new ScriptedAgentRuntime() : new PiAgentRuntime({ sessionRoot: env.piSessionRecording ? piSessionsRoot(env.dataDir) : undefined, }); const notifications = new ExpoPushProvider(env.dataDir); const auth = createAuth(prisma, { secret: env.authSecret, baseURL: env.authUrl, webOrigin: env.webOrigin, signupsEnabled: env.signupsEnabled, signupAllowlist: env.signupAllowlist, email, onEmailError: (error) => getLogger().error("transactional email delivery failed", error), extraOrigins: [ "rakazo://", "exp://", "exp://*", "http://localhost:8081", "http://127.0.0.1:8081", "http://localhost:19006", "http://127.0.0.1:19006", "http://*:5173", "http://*:3100", "https://*:5173", "https://*:3100", "http://192.168.*:*", "https://192.168.*:*", "http://100.*:*", "https://100.*:*", "http://10.*:*", "https://10.*:*", "http://172.*:*", "https://172.*:*", "http://*.local:*", "https://*.local:*", ...(process.env.EXTRA_TRUSTED_ORIGINS?.split(",").map((s) => s.trim()) ?? []), ], beforeDeleteUser: async (userId) => { const bots = await prisma.bot.findMany({ where: { userId }, select: { id: true, userId: true, spaceId: true, name: true, archivedAt: true }, }); await Promise.all( bots.map((bot) => destroyBot( { prisma, sandbox, home, jobs, artifacts, dataDir: env.dataDir }, bot, { operationId: `account-delete:${userId}`, traceId: `account-delete:${userId}`, spaceId: bot.spaceId, userId, botId: bot.id, signal: new AbortController().signal, }, { deleteMemories: true }, ), ), ); await removePiUserSessions(env.dataDir, userId); await rm(pushTokenPath(env.dataDir, userId), { force: true }).catch(() => undefined); }, }); // One provider instance so emulator launches and polls share the same Map. const cloudAgent = createCloudAgentConnection({ CLOUD_AGENT_PROVIDER: env.cloudAgentProvider, CURSOR_API_KEY: env.cursorApiKey, CLOUD_AGENT_SPACE_ID: env.cloudAgentSpaceId, }); const shutdown = new AbortController(); const executor = createRunExecutor({ prisma, runtime, sandbox, memory, memoryProviders, home, artifacts, connector: stack.connector, connectors: stack.connector, listConnectedPluginSlugs: async (userId) => { const provider = await integrationSettings.resolve("composio"); if (!provider) return []; return provider.listConnectedExternalIds({ userId, spaceId: "", operationId: "connections.sync", traceId: "connections.sync", signal: AbortSignal.timeout(15_000), }); }, secrets: [ env.deploymentModelKey ?? "", env.composioApiKey ?? "", env.cursorApiKey ?? "", ].filter(Boolean), secretStore: secrets, secretHttp: remoteConnectors, deploymentModelKey: env.deploymentModelKey, dataDir: env.dataDir, notifications, jobs, events, messaging: messaging ? createMessagingContextLoader(prisma) : undefined, web: createWebProvider(), cloudAgent, shutdownSignal: shutdown.signal, }); const jobHandlers = createBackgroundJobHandlers({ executor, prisma, sandbox, home, jobs, events, workerId: "api", runtime, secretStore: secrets, memoryProviders, deploymentModelKey: env.deploymentModelKey, messaging, cloudAgent, }); if (inMemoryJobs) { await inMemoryJobs.start(jobHandlers); } const reconciler = inMemoryJobs ? createJobReconciler({ prisma, jobs, reconcileCloudAgents: () => reconcileCloudAgents({ prisma, jobs, cloudAgent }), reconcileComputerUpdates: () => reconcileComputerUpdates({ prisma, jobs }), }) : undefined; reconciler?.start(); const router = createRouter({ cloudAgent, prisma, events, auth, jobs, sandbox, memory, memoryProviders, home, secrets, oauthLogins, integrationSettings, mcpOAuth, composio: stack.composio, connectors: stack.connector, remoteConnectors, artifacts, dataDir: env.dataDir, messaging: { enabled: Boolean(messaging), providers: messaging?.platforms().map((platform) => platform.provider) ?? [], openSignup: env.messagingOpenSignup, }, env: { agentRuntime: env.agentRuntime, defaultProvider: env.defaultProvider, defaultModel: env.defaultModel, teamChatJudgeProvider: env.teamChatJudgeProvider, teamChatJudgeModel: env.teamChatJudgeModel, deploymentModelKey: env.deploymentModelKey, webOrigin: env.webOrigin, privacyPolicyUrl: env.privacyPolicyUrl, screenProxySecret: env.screenProxySecret, sandboxProvider: env.sandboxProvider, gitSha: env.gitSha, updaterUrl: env.updaterUrl, updaterToken: env.updaterToken, imageTag: env.imageTag, integrationsCatalogUrl: env.integrationsCatalogUrl, }, }); const rpc = new RPCHandler(router, { clientInterceptors: [onError((error, { path }) => logUnexpectedRpcError(error, path))], }); const app = new Hono(); app.use("*", requestLogging(logger)); app.use( "*", cors({ origin: (origin) => { if (!origin) return env.webOrigin; return isTrustedOrigin(origin, env) ? origin : ""; }, credentials: true, }), ); app.get("/api/auth/capabilities", (c) => c.json({ passwordReset: Boolean(email), resetUrl: email ? new URL("/reset-password", env.webOrigin).href : null, }), ); if (localEmailEmulator && env.nodeEnv === "development") { app.get( "/api/dev/emails", () => new Response(JSON.stringify(localEmailEmulator.sent), { headers: { "cache-control": "no-store", "content-type": "application/json" }, }), ); } mountApiRequestBodyLimits(app); mountScreenTarget(app, prisma, env.screenProxySecret); app.on(["GET", "POST"], "/api/auth/*", async (c) => { const path = new URL(c.req.url).pathname.replace("/api/auth", ""); if (blockedAuthPaths.some((blocked) => path.startsWith(blocked))) { return c.json({ error: "Not available in version 1" }, 404); } return auth.handler(c.req.raw); }); mountLocalSettings(app, { token: env.desktopStackToken, prisma, rpc }); app.use("/rpc/*", async (c, next) => { const session = await auth.api.getSession({ headers: sessionHeaders(c.req.raw) }); const requestedSpaceId = c.req.header("x-rakazo-space-id"); const actor = session?.user ? await requireMembership(prisma, session.user.id, requestedSpaceId).catch(() => null) : null; if (actor) { enrichLogContext({ "user.id": actor.userId, "space.id": actor.spaceId }); } const { matched, response } = await rpc.handle(c.req.raw, { prefix: "/rpc", context: { actor, signal: c.req.raw.signal }, }); if (matched) return c.newResponse(response.body, response); await next(); }); mountVoiceHttpRoutes(app, { prisma, secrets }, async (c) => { const session = await auth.api.getSession({ headers: sessionHeaders(c.req.raw) }); if (!session?.user) return null; const actor = await requireMembership( prisma, session.user.id, c.req.header("x-rakazo-space-id"), ).catch(() => null); if (actor) enrichLogContext({ "user.id": actor.userId, "space.id": actor.spaceId }); return actor; }); mountWebhookHttpRoutes(app, { prisma, secrets, events, jobs }); // Shared with stop so a shutdown during retry delays does not restart polling. let messagingStopped = false; let clearMessagingRetryDelay: (() => void) | undefined; let messagingInitTask: Promise | undefined; let clearTeamChatRetryDelay: (() => void) | undefined; let teamChatInitTask: Promise | undefined; // Messaging webhooks only exist when the surface is enabled. let teamChatBridge: TeamChatBridge | undefined; /** Constructed even before start() succeeds so stop() can cancel in-flight startup. */ let teamChatBridgeInstance: TeamChatBridge | undefined; const pendingTeamChatInbound = new PendingTeamChatInbound(); if (messaging) { const inboundDeps = { prisma, events, jobs, provision: (request, policyEnv) => provisionMessagingIdentity(prisma, request, policyEnv), openSignup: env.messagingOpenSignup, signupPolicy: { signupsEnabled: env.signupsEnabled, signupAllowlist: env.signupAllowlist, }, typing: (threadId) => { // Keep conversation addresses out of trace ids — those reach logs // and telemetry, a different trust boundary than the database. const operationId = `messaging.typing:${randomUUID()}`; return messaging.sendTyping(threadId, { operationId, traceId: operationId, spaceId: "", userId: "", // Cosmetic side call: the wait is bounded so a stalled vendor // response never holds our callback chain (the Chat SDK adapter // API cannot cancel the underlying request itself). signal: AbortSignal.timeout(2000), }); }, } satisfies Parameters[0]; const inbound = createMessagingInboundHandler(inboundDeps); const handleTeamChatInbound = async ( bridge: TeamChatBridge, event: Parameters[2], ) => { const mapped = toTeamChatInbound(event); if (!mapped) { await inbound(event); return; } const canWake = await teamChatSenderCanWakeMessageRoutines(inboundDeps, event); if (!canWake) { await bridge.receive(mapped); return; } // Persist a non-reconcilable row until routine routing owns or releases // the message, so the timer cannot start a second TeamChat run. const target = await bridge.receive(mapped, { queueAgent: false }); if (!target.deferred) return; const leaseHeartbeat = await bridge.startDeferredReservationHeartbeat( target.externalMessageId, ); let woken = false; bridge.markRoutineWakeInFlight(target.externalMessageId); try { const wakePromise = wakeMessageRoutines(inboundDeps, target, event, { // Must match TeamChatBridge ExternalConversation / recovery provider. deliveryProvider: bridge.providerId, externalMessageId: target.externalMessageId, }); try { woken = await Promise.race([wakePromise, leaseHeartbeat.lost]); } catch (error) { if (isDeferredReservationLost(error)) { // Lease loss must not start a fallback agent beside an in-flight // wake: re-hold exclusive ownership while awaiting that wake, then // resolve from its settled result. let hold: { stop: () => void } | undefined; try { hold = await bridge.startDeferredReservationHeartbeat(target.externalMessageId); } catch { // Row already left deferred; wake CAS / resolve decide the winner. } try { try { woken = await wakePromise; } catch (wakeError) { const released = await bridge.resolveDeferredMessage( target.externalMessageId, "agent", mapped.kind, ); if (!released) { throw new Error("Team chat deferred message ownership conflict", { cause: wakeError, }); } await bridge.reconcileOnce(); getLogger().error( "team chat routine wake failed after deferred lease loss", wakeError, ); return; } } finally { hold?.stop(); } } else { await bridge.resolveDeferredMessage(target.externalMessageId, "agent", mapped.kind); await bridge.reconcileOnce(); throw error; } } const resolved = await bridge.resolveDeferredMessage( target.externalMessageId, woken ? "routine" : "agent", mapped.kind, ); if (!resolved) { throw new Error("Team chat deferred message ownership conflict"); } if (!woken) await bridge.reconcileOnce(); } finally { bridge.clearRoutineWakeInFlight(target.externalMessageId); leaseHeartbeat.stop(); } }; const flushPendingTeamChatInbound = (bridge: TeamChatBridge) => { pendingTeamChatInbound.flush((event) => handleTeamChatInbound(bridge, event)); }; if (env.teamChatBotId) { const judge = env.teamChatJudgeProvider && env.teamChatJudgeModel ? new ModelTeamChatEngagementJudge({ prisma, runtime, secrets, deploymentProvider: env.defaultProvider, deploymentModel: env.defaultModel, deploymentModelKey: env.deploymentModelKey, providerOverride: env.teamChatJudgeProvider, modelOverride: env.teamChatJudgeModel, }) : new ModelTeamChatEngagementJudge({ prisma, runtime, secrets, deploymentProvider: env.defaultProvider, deploymentModel: env.defaultModel, deploymentModelKey: env.deploymentModelKey, }); const bridge = new TeamChatBridge({ prisma, events, jobs, send: createMessagingTeamChatSender(messaging), providerId: "slack", botId: env.teamChatBotId, judge, }); teamChatBridgeInstance = bridge; try { await bridge.start(); teamChatBridge = bridge; } catch (error) { getLogger().error("team chat bridge failed to start; retrying", error); teamChatInitTask = (async () => { let delayMs = 2_000; while (!messagingStopped) { await new Promise((resolve) => { const timer = setTimeout(resolve, delayMs); clearTeamChatRetryDelay = () => { clearTimeout(timer); clearTeamChatRetryDelay = undefined; resolve(); }; }); clearTeamChatRetryDelay = undefined; if (messagingStopped) return; try { await bridge.start(); if (messagingStopped) { await bridge.stop(); return; } teamChatBridge = bridge; flushPendingTeamChatInbound(bridge); return; } catch (retryError) { if ( retryError instanceof Error && retryError.message === "Team chat bridge start cancelled" ) { return; } getLogger().error("team chat bridge failed to start; retrying", retryError); delayMs = Math.min(delayMs * 5, 30_000); } } })(); } } messaging.onInbound(async (event) => { if (event.type !== "message") { await applyMessagingOutboundStatus(prisma, event); return; } if (prefersTeamChatSurface(event, env.teamChatBotId)) { const bridge = teamChatBridge; if (bridge) { await handleTeamChatInbound(bridge, event); return; } // Bridge is still starting (or retrying). Do not fall through to the // personal-line inbound path — that bypasses externalMessage ownership // and can wake routines for unlinked TeamChat senders. if (teamChatInitTask) { const pending = pendingTeamChatInbound.enqueue(event); if (!pending) { throw new Error("Team chat inbound buffer is full"); } await pending; return; } throw new Error("Team chat bridge is unavailable"); } await inbound(event); }); mountMessagingWebhookRoutes(app, { messaging }); // Start polling-mode adapters (e.g. Telegram with no public webhook URL // registered) immediately rather than waiting for the first webhook // POST or outbound send to lazily trigger it. This is the process that // owns the inbound sink registered just above, so it must be the one // holding the live connection — a second poller elsewhere (e.g. the // worker) would only fight this one for Telegram's single getUpdates // slot without ever seeing the messages itself. // Bounded retries cover transient Telegram startup failures; polling-only // bots otherwise stay dark until an unrelated outbound send re-inits. messagingInitTask = (async () => { const delayMs = [0, 2_000, 10_000]; for (let attempt = 0; attempt < delayMs.length; attempt += 1) { if (messagingStopped) return; if (delayMs[attempt]! > 0) { await new Promise((resolve) => { const timer = setTimeout(resolve, delayMs[attempt]); clearMessagingRetryDelay = () => { clearTimeout(timer); clearMessagingRetryDelay = undefined; resolve(); }; }); clearMessagingRetryDelay = undefined; } if (messagingStopped) return; try { await messaging.initialize?.(); return; } catch (error) { getLogger().error( attempt === delayMs.length - 1 ? "messaging surface initialize failed" : "messaging surface initialize failed; retrying", error, ); } } })(); } app.get("/health", (c) => c.json({ ok: true, runtime: env.agentRuntime, sandbox: env.sandboxProvider, composio: Boolean(stack.composio), pipedream: Boolean(pipedream), messaging: Boolean(messaging), email: email?.describe().id ?? null, jobs: jobKind, realtime: realtime.describe().id, revision: env.gitSha ?? null, }), ); return { app, prisma, jobs, sandbox, connector, composio: stack.composio, connectors: stack.connector, messaging, email, executor, runtime, stop: async () => { // Abort in-flight continueRun boot waits before draining jobs so stop() cannot sit // on waitForComputerReady for the full boot-wait window during shared Postgres journeys. shutdown.abort(); oauthLogins.abortAll(); messagingStopped = true; clearMessagingRetryDelay?.(); clearTeamChatRetryDelay?.(); pendingTeamChatInbound.reject(new Error("Team chat bridge stopped before startup")); // Cancel in-flight start() before awaiting the retry task so stop() cannot // sit on DB/reconcile work that bridge.start() is still running. await settleWithTimeout( teamChatBridgeInstance ? teamChatBridgeInstance.stop().catch(() => undefined) : undefined, TEAM_CHAT_STARTUP_SHUTDOWN_MS, ); await messagingInitTask?.catch(() => undefined); await settleWithTimeout(teamChatInitTask, TEAM_CHAT_STARTUP_SHUTDOWN_MS); await messaging?.shutdown?.(); await teamChatBridge?.stop(); await email?.drain?.(); await reconciler?.stop(); await jobs.close(); await realtime.close(); await connector.stop(); await mcp.close(); await prisma.$disconnect().catch(() => undefined); await created.pool?.end().catch(() => undefined); await ownedJobPool?.end().catch(() => undefined); await logger.flush({ timeoutMs: 2_000 }); }, }; } function isTrustedOrigin(origin: string, env: AppEnv) { if (!origin) return true; if (origin === env.webOrigin || origin === env.apiUrl || origin === env.authUrl) return true; if (origin.startsWith("rakazo://") || origin.startsWith("exp://")) return true; try { const host = new URL(origin).hostname; return isPrivateOrLocalHost(host); } catch { return false; } } function isPrivateOrLocalHost(host: string): boolean { if (isLoopbackHost(host)) return true; if (host.endsWith(".local")) return true; if (/^192\.168\.\d{1,3}\.\d{1,3}$/.test(host)) return true; if (/^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host)) return true; if (/^172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}$/.test(host)) return true; if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.\d{1,3}\.\d{1,3}$/.test(host)) return true; return false; } function isLoopbackHost(host: string): boolean { return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]"; } function sessionHeaders(request: Request) { const headers = new Headers(request.headers); const authz = headers.get("authorization"); if (authz?.toLowerCase().startsWith("bearer ") && !headers.get("cookie")) { headers.set("cookie", `better-auth.session_token=${authz.slice(7).trim()}`); } return headers; } /** * An ORPCError is a decision the router made (BAD_REQUEST, UNAUTHORIZED, ...) and reaches the * caller intact. Everything else is flattened into an opaque "Internal server error", so * unless it is logged here the only record of what actually broke is gone. * * The cause chain matters as much as the message: undici and most SDKs report a bare * "fetch failed" and keep the host and errno one level down. */ export function logUnexpectedRpcError(error: unknown, path: readonly string[]): void { if (error instanceof ORPCError) return; const where = `rpc ${path.join("/")} failed`; getLogger().error(where, error); }