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
38 changes: 27 additions & 11 deletions apps/sim/app/api/admin/mothership/route.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { db } from '@sim/db'
import { user } from '@sim/db/schema'
import { settings, user } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { adminMothershipQuerySchema } from '@/lib/api/contracts/mothership-tasks'
import { mothershipEnvironmentSchema } from '@/lib/api/contracts/user'
import { searchParamsToObject, validationErrorResponse } from '@/lib/api/server'
import { getSession } from '@/lib/auth'
import { getMothershipBaseURL } from '@/lib/copilot/server/agent-url'
import { env } from '@/lib/core/config/env'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'

Expand All @@ -14,8 +16,15 @@ const ENV_URLS: Record<string, string | undefined> = {
prod: env.MOTHERSHIP_PROD_URL,
}

function getMothershipUrl(environment: string): string | null {
return ENV_URLS[environment] ?? null
async function getMothershipUrl(environment: string, userId: string): Promise<string | null> {
const parsedEnvironment = mothershipEnvironmentSchema.safeParse(environment)
if (!parsedEnvironment.success) return ENV_URLS[environment] ?? null

return getMothershipBaseURL({
userId,
environment: parsedEnvironment.data,
fallbackUrl: ENV_URLS[environment],
})
}

const ENDPOINT_PATTERN = /^[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)*$/
Expand All @@ -26,17 +35,22 @@ function isValidEndpoint(endpoint: string): boolean {
return ENDPOINT_PATTERN.test(endpoint)
}

async function isAdminRequestAuthorized() {
async function getAuthorizedAdminUserId() {
const session = await getSession()
if (!session?.user?.id) return false
if (!session?.user?.id) return null

const [currentUser] = await db
.select({ role: user.role })
.select({
role: user.role,
superUserModeEnabled: settings.superUserModeEnabled,
})
.from(user)
.leftJoin(settings, eq(settings.userId, user.id))
.where(eq(user.id, session.user.id))
.limit(1)

return currentUser?.role === 'admin'
const authorized = currentUser?.role === 'admin' && (currentUser.superUserModeEnabled ?? false)
return authorized ? session.user.id : null
}

/**
Expand All @@ -50,7 +64,8 @@ async function isAdminRequestAuthorized() {
* (e.g. requestId for GET /traces) are forwarded.
*/
export const POST = withRouteHandler(async (req: NextRequest) => {
if (!(await isAdminRequestAuthorized())) {
const userId = await getAuthorizedAdminUserId()
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

Expand All @@ -68,7 +83,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
return NextResponse.json({ error: 'invalid endpoint' }, { status: 400 })
}

const baseUrl = getMothershipUrl(environment)
const baseUrl = await getMothershipUrl(environment, userId)
if (!baseUrl) {
return NextResponse.json(
{ error: `No URL configured for environment: ${environment}` },
Expand Down Expand Up @@ -102,7 +117,8 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
})

export const GET = withRouteHandler(async (req: NextRequest) => {
if (!(await isAdminRequestAuthorized())) {
const userId = await getAuthorizedAdminUserId()
if (!userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

Expand All @@ -120,7 +136,7 @@ export const GET = withRouteHandler(async (req: NextRequest) => {
return NextResponse.json({ error: 'invalid endpoint' }, { status: 400 })
}

const baseUrl = getMothershipUrl(environment)
const baseUrl = await getMothershipUrl(environment, userId)
if (!baseUrl) {
return NextResponse.json(
{ error: `No URL configured for environment: ${environment}` },
Expand Down
5 changes: 3 additions & 2 deletions apps/sim/app/api/copilot/api-keys/generate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { type NextRequest, NextResponse } from 'next/server'
import { generateCopilotApiKeyContract } from '@/lib/api/contracts'
import { parseRequest } from '@/lib/api/server'
import { getSession } from '@/lib/auth'
import { SIM_AGENT_API_URL } from '@/lib/copilot/constants'
import { TraceAttr } from '@/lib/copilot/generated/trace-attributes-v1'
import { fetchGo } from '@/lib/copilot/request/go/fetch'
import { getMothershipBaseURL } from '@/lib/copilot/server/agent-url'
import { env } from '@/lib/core/config/env'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'

Expand All @@ -16,13 +16,14 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
}

const userId = session.user.id
const mothershipBaseURL = await getMothershipBaseURL({ userId })

const parsed = await parseRequest(generateCopilotApiKeyContract, req, {})
if (!parsed.success) return parsed.response

const { name } = parsed.data.body

const res = await fetchGo(`${SIM_AGENT_API_URL}/api/validate-key/generate`, {
const res = await fetchGo(`${mothershipBaseURL}/api/validate-key/generate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down
8 changes: 7 additions & 1 deletion apps/sim/app/api/copilot/api-keys/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import { authMockFns, createEnvMock } from '@sim/testing'
import { NextRequest } from 'next/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'

const { mockFetch } = vi.hoisted(() => ({
const { mockFetch, mockGetMothershipBaseURL } = vi.hoisted(() => ({
mockFetch: vi.fn(),
mockGetMothershipBaseURL: vi.fn(),
}))

vi.mock('@/lib/copilot/constants', () => ({
Expand All @@ -18,6 +19,10 @@ vi.mock('@/lib/copilot/constants', () => ({
COPILOT_REQUEST_MODES: ['ask', 'build', 'plan', 'agent'] as const,
}))

vi.mock('@/lib/copilot/server/agent-url', () => ({
getMothershipBaseURL: mockGetMothershipBaseURL,
}))

vi.mock('@/lib/core/config/env', () => createEnvMock({ COPILOT_API_KEY: 'test-api-key' }))

import { DELETE, GET } from '@/app/api/copilot/api-keys/route'
Expand All @@ -41,6 +46,7 @@ function buildMockResponse(init: {
describe('Copilot API Keys API Route', () => {
beforeEach(() => {
vi.clearAllMocks()
mockGetMothershipBaseURL.mockResolvedValue('https://agent.sim.example.com')
global.fetch = mockFetch
})

Expand Down
8 changes: 5 additions & 3 deletions apps/sim/app/api/copilot/api-keys/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { type NextRequest, NextResponse } from 'next/server'
import { deleteCopilotApiKeyQuerySchema } from '@/lib/api/contracts'
import { getSession } from '@/lib/auth'
import { SIM_AGENT_API_URL } from '@/lib/copilot/constants'
import { TraceAttr } from '@/lib/copilot/generated/trace-attributes-v1'
import { fetchGo } from '@/lib/copilot/request/go/fetch'
import { getMothershipBaseURL } from '@/lib/copilot/server/agent-url'
import { env } from '@/lib/core/config/env'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'

Expand All @@ -15,8 +15,9 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
}

const userId = session.user.id
const mothershipBaseURL = await getMothershipBaseURL({ userId })

const res = await fetchGo(`${SIM_AGENT_API_URL}/api/validate-key/get-api-keys`, {
const res = await fetchGo(`${mothershipBaseURL}/api/validate-key/get-api-keys`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down Expand Up @@ -67,6 +68,7 @@ export const DELETE = withRouteHandler(async (request: NextRequest) => {
}

const userId = session.user.id
const mothershipBaseURL = await getMothershipBaseURL({ userId })
const queryResult = deleteCopilotApiKeyQuerySchema.safeParse(
Object.fromEntries(new URL(request.url).searchParams)
)
Expand All @@ -75,7 +77,7 @@ export const DELETE = withRouteHandler(async (request: NextRequest) => {
}
const { id } = queryResult.data

const res = await fetchGo(`${SIM_AGENT_API_URL}/api/validate-key/delete`, {
const res = await fetchGo(`${mothershipBaseURL}/api/validate-key/delete`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down
11 changes: 7 additions & 4 deletions apps/sim/app/api/copilot/auto-allowed-tools/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import {
} from '@/lib/api/contracts/copilot'
import { parseRequest } from '@/lib/api/server'
import { getSession } from '@/lib/auth'
import { SIM_AGENT_API_URL } from '@/lib/copilot/constants'
import { TraceAttr } from '@/lib/copilot/generated/trace-attributes-v1'
import { fetchGo } from '@/lib/copilot/request/go/fetch'
import { getMothershipBaseURL } from '@/lib/copilot/server/agent-url'
import { env } from '@/lib/core/config/env'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'

Expand Down Expand Up @@ -37,9 +37,10 @@ export const GET = withRouteHandler(async () => {
}

const userId = session.user.id
const mothershipBaseURL = await getMothershipBaseURL({ userId })

const res = await fetchGo(
`${SIM_AGENT_API_URL}/api/tool-preferences/auto-allowed?userId=${encodeURIComponent(userId)}`,
`${mothershipBaseURL}/api/tool-preferences/auto-allowed?userId=${encodeURIComponent(userId)}`,
{
method: 'GET',
headers: copilotHeaders(),
Expand Down Expand Up @@ -74,6 +75,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
}

const userId = session.user.id
const mothershipBaseURL = await getMothershipBaseURL({ userId })
const parsed = await parseRequest(
addCopilotAutoAllowedToolContract,
request,
Expand All @@ -88,7 +90,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
if (!parsed.success) return parsed.response
const { toolId } = parsed.data.body

const res = await fetchGo(`${SIM_AGENT_API_URL}/api/tool-preferences/auto-allowed`, {
const res = await fetchGo(`${mothershipBaseURL}/api/tool-preferences/auto-allowed`, {
method: 'POST',
headers: copilotHeaders(),
body: JSON.stringify({ userId, toolId }),
Expand Down Expand Up @@ -125,6 +127,7 @@ export const DELETE = withRouteHandler(async (request: NextRequest) => {
}

const userId = session.user.id
const mothershipBaseURL = await getMothershipBaseURL({ userId })
const parsed = await parseRequest(
removeCopilotAutoAllowedToolContract,
request,
Expand All @@ -138,7 +141,7 @@ export const DELETE = withRouteHandler(async (request: NextRequest) => {
const { toolId } = parsed.data.query

const res = await fetchGo(
`${SIM_AGENT_API_URL}/api/tool-preferences/auto-allowed?userId=${encodeURIComponent(userId)}&toolId=${encodeURIComponent(toolId)}`,
`${mothershipBaseURL}/api/tool-preferences/auto-allowed?userId=${encodeURIComponent(userId)}&toolId=${encodeURIComponent(toolId)}`,
{
method: 'DELETE',
headers: copilotHeaders(),
Expand Down
6 changes: 4 additions & 2 deletions apps/sim/app/api/copilot/chat/abort/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { type NextRequest, NextResponse } from 'next/server'
import { copilotChatAbortBodySchema } from '@/lib/api/contracts/copilot'
import { validationErrorResponse } from '@/lib/api/server'
import { getLatestRunForStream } from '@/lib/copilot/async-runs/repository'
import { SIM_AGENT_API_URL } from '@/lib/copilot/constants'
import { CopilotAbortOutcome } from '@/lib/copilot/generated/trace-attribute-values-v1'
import { TraceAttr } from '@/lib/copilot/generated/trace-attributes-v1'
import { TraceSpan } from '@/lib/copilot/generated/trace-spans-v1'
import { fetchGo } from '@/lib/copilot/request/go/fetch'
import { authenticateCopilotRequestSessionOnly } from '@/lib/copilot/request/http'
import { withCopilotSpan, withIncomingGoSpan } from '@/lib/copilot/request/otel'
import { abortActiveStream, waitForPendingChatStream } from '@/lib/copilot/request/session'
import { getMothershipBaseURL, getMothershipSourceEnvHeaders } from '@/lib/copilot/server/agent-url'
import { env } from '@/lib/core/config/env'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'

Expand Down Expand Up @@ -85,12 +85,14 @@ export const POST = withRouteHandler((request: NextRequest) =>
if (env.COPILOT_API_KEY) {
headers['x-api-key'] = env.COPILOT_API_KEY
}
Object.assign(headers, getMothershipSourceEnvHeaders())
const controller = new AbortController()
const timeout = setTimeout(
() => controller.abort('timeout:go_explicit_abort_fetch'),
GO_EXPLICIT_ABORT_TIMEOUT_MS
)
const response = await fetchGo(`${SIM_AGENT_API_URL}/api/streams/explicit-abort`, {
const mothershipBaseURL = await getMothershipBaseURL({ userId: authenticatedUserId })
const response = await fetchGo(`${mothershipBaseURL}/api/streams/explicit-abort`, {
method: 'POST',
headers,
signal: controller.signal,
Expand Down
5 changes: 3 additions & 2 deletions apps/sim/app/api/copilot/models/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { copilotModelsContract } from '@/lib/api/contracts/copilot'
import { parseRequest } from '@/lib/api/server'
import { SIM_AGENT_API_URL } from '@/lib/copilot/constants'
import { fetchGo } from '@/lib/copilot/request/go/fetch'
import { authenticateCopilotRequestSessionOnly } from '@/lib/copilot/request/http'
import { getMothershipBaseURL } from '@/lib/copilot/server/agent-url'
import { env } from '@/lib/core/config/env'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'

Expand Down Expand Up @@ -50,7 +50,8 @@ export const GET = withRouteHandler(async (req: NextRequest) => {
}

try {
const response = await fetchGo(`${SIM_AGENT_API_URL}/api/get-available-models`, {
const mothershipBaseURL = await getMothershipBaseURL({ userId })
const response = await fetchGo(`${mothershipBaseURL}/api/get-available-models`, {
method: 'GET',
headers,
cache: 'no-store',
Expand Down
8 changes: 7 additions & 1 deletion apps/sim/app/api/copilot/stats/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import { copilotHttpMock, copilotHttpMockFns, createEnvMock, createMockRequest }
import { NextRequest } from 'next/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'

const { mockFetch } = vi.hoisted(() => ({
const { mockFetch, mockGetMothershipBaseURL } = vi.hoisted(() => ({
mockFetch: vi.fn(),
mockGetMothershipBaseURL: vi.fn(),
}))

vi.mock('@/lib/copilot/request/http', () => copilotHttpMock)
Expand All @@ -20,6 +21,10 @@ vi.mock('@/lib/copilot/constants', () => ({
COPILOT_REQUEST_MODES: ['ask', 'build', 'plan', 'agent'] as const,
}))

vi.mock('@/lib/copilot/server/agent-url', () => ({
getMothershipBaseURL: mockGetMothershipBaseURL,
}))

vi.mock('@/lib/core/config/env', () => createEnvMock({ COPILOT_API_KEY: 'test-api-key' }))

import { POST } from '@/app/api/copilot/stats/route'
Expand All @@ -43,6 +48,7 @@ function buildMockResponse(init: {
describe('Copilot Stats API Route', () => {
beforeEach(() => {
vi.clearAllMocks()
mockGetMothershipBaseURL.mockResolvedValue('https://agent.sim.example.com')
global.fetch = mockFetch
})

Expand Down
5 changes: 3 additions & 2 deletions apps/sim/app/api/copilot/stats/route.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { type NextRequest, NextResponse } from 'next/server'
import { copilotStatsContract } from '@/lib/api/contracts/copilot'
import { parseRequest, validationErrorResponse } from '@/lib/api/server'
import { SIM_AGENT_API_URL } from '@/lib/copilot/constants'
import { fetchGo } from '@/lib/copilot/request/go/fetch'
import {
authenticateCopilotRequestSessionOnly,
createInternalServerErrorResponse,
createRequestTracker,
createUnauthorizedResponse,
} from '@/lib/copilot/request/http'
import { getMothershipBaseURL } from '@/lib/copilot/server/agent-url'
import { env } from '@/lib/core/config/env'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'

Expand Down Expand Up @@ -44,7 +44,8 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
diffAccepted,
}

const agentRes = await fetchGo(`${SIM_AGENT_API_URL}/api/stats`, {
const mothershipBaseURL = await getMothershipBaseURL({ userId })
const agentRes = await fetchGo(`${mothershipBaseURL}/api/stats`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down
6 changes: 4 additions & 2 deletions apps/sim/app/api/mothership/chats/[chatId]/fork/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { type NextRequest, NextResponse } from 'next/server'
import { forkMothershipChatContract } from '@/lib/api/contracts/mothership-tasks'
import { parseRequest } from '@/lib/api/server'
import type { PersistedMessage } from '@/lib/copilot/chat/persisted-message'
import { SIM_AGENT_API_URL } from '@/lib/copilot/constants'
import { fetchGo } from '@/lib/copilot/request/go/fetch'
import {
authenticateCopilotRequestSessionOnly,
Expand All @@ -17,6 +16,7 @@ import {
createUnauthorizedResponse,
} from '@/lib/copilot/request/http'
import type { MothershipResource } from '@/lib/copilot/resources/types'
import { getMothershipBaseURL, getMothershipSourceEnvHeaders } from '@/lib/copilot/server/agent-url'
import { taskPubSub } from '@/lib/copilot/tasks'
import { env } from '@/lib/core/config/env'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
Expand Down Expand Up @@ -109,7 +109,9 @@ export const POST = withRouteHandler(
if (env.COPILOT_API_KEY) {
copilotHeaders['x-api-key'] = env.COPILOT_API_KEY
}
const copilotRes = await fetchGo(`${SIM_AGENT_API_URL}/api/chats/fork`, {
Object.assign(copilotHeaders, getMothershipSourceEnvHeaders())
const mothershipBaseURL = await getMothershipBaseURL({ userId })
const copilotRes = await fetchGo(`${mothershipBaseURL}/api/chats/fork`, {
method: 'POST',
headers: copilotHeaders,
body: JSON.stringify({
Expand Down
Loading
Loading