feat(mcp): added support for mcp servers (#1296)

* update infra and remove railway

* feat(mcp): add mcp support

* consolidate mcp utils

* UI improvements, more MCP stuff

* cleanup placeholders

* reran migrations

* general improvements

* fix server side mcp exec

* more improvements, fixed search in environment settings tab

* persist subblock values for mcp block

* style fixes

* udpdate all text-primary to text-muted-foreground for visibility in dark mode

* Revert "update infra and remove railway"

This reverts commit dbf2b153b8f96808e7bb7e5f86f7e8624e3c12dd.

* make MCP servers workspace-scoped

* cleanup & remove unused dep

* consolidated utils, DRY

* added tests

* better error messages, confirmed that permissions works correctly

* additional improvements

* remove extraneous comments

* reran migrations

* lint

* style changes

* fix: prevent config mutation in MCP client URL retry logic

Fixed an issue where the MCP client was mutating the shared configuration
object's URL during retry attempts. This could cause configuration corruption
if the same config object was reused elsewhere.

* resolve PR comments

* ack PR comments
This commit is contained in:
Waleed
2025-09-09 17:18:08 -07:00
committed by GitHub
parent c48039f97f
commit 3d4b9f0665
193 changed files with 14250 additions and 603 deletions
+1 -1
View File
@@ -101,7 +101,7 @@ function ResetPasswordContent() {
</CardContent>
<CardFooter>
<p className='w-full text-center text-gray-500 text-sm'>
<Link href='/login' className='text-primary hover:underline'>
<Link href='/login' className='text-muted-foreground hover:underline'>
Back to login
</Link>
</p>
-5
View File
@@ -3,7 +3,6 @@
*/
import { afterEach, beforeEach, vi } from 'vitest'
// Mock Next.js implementations
vi.mock('next/headers', () => ({
cookies: () => ({
get: vi.fn().mockReturnValue({ value: 'test-session-token' }),
@@ -13,7 +12,6 @@ vi.mock('next/headers', () => ({
}),
}))
// Mock auth utilities
vi.mock('@/lib/auth/session', () => ({
getSession: vi.fn().mockResolvedValue({
user: {
@@ -24,13 +22,10 @@ vi.mock('@/lib/auth/session', () => ({
}),
}))
// Configure Vitest environment
beforeEach(() => {
// Clear all mocks before each test
vi.clearAllMocks()
})
afterEach(() => {
// Ensure all mocks are restored after each test
vi.restoreAllMocks()
})
-18
View File
@@ -944,12 +944,10 @@ export interface TestSetupOptions {
export function setupComprehensiveTestMocks(options: TestSetupOptions = {}) {
const { auth = { authenticated: true }, database = {}, storage, authApi, features = {} } = options
// Setup basic infrastructure mocks
setupCommonApiMocks()
mockUuid()
mockCryptoUuid()
// Setup authentication
const authMocks = mockAuth(auth.user)
if (auth.authenticated) {
authMocks.setAuthenticated(auth.user)
@@ -957,22 +955,18 @@ export function setupComprehensiveTestMocks(options: TestSetupOptions = {}) {
authMocks.setUnauthenticated()
}
// Setup database
const dbMocks = createMockDatabase(database)
// Setup storage if needed
let storageMocks
if (storage) {
storageMocks = createStorageProviderMocks(storage)
}
// Setup auth API if needed
let authApiMocks
if (authApi) {
authApiMocks = createAuthApiMocks(authApi)
}
// Setup feature-specific mocks
const featureMocks: any = {}
if (features.workflowUtils) {
featureMocks.workflowUtils = mockWorkflowUtils()
@@ -1008,12 +1002,10 @@ export function createMockDatabase(options: MockDatabaseOptions = {}) {
let selectCallCount = 0
// Helper to create error
const createDbError = (operation: string, message?: string) => {
return new Error(message || `Database ${operation} error`)
}
// Create chainable select mock
const createSelectChain = () => ({
from: vi.fn().mockReturnThis(),
leftJoin: vi.fn().mockReturnThis(),
@@ -1038,7 +1030,6 @@ export function createMockDatabase(options: MockDatabaseOptions = {}) {
}),
})
// Create insert chain
const createInsertChain = () => ({
values: vi.fn().mockImplementation(() => ({
returning: vi.fn().mockImplementation(() => {
@@ -1056,7 +1047,6 @@ export function createMockDatabase(options: MockDatabaseOptions = {}) {
})),
})
// Create update chain
const createUpdateChain = () => ({
set: vi.fn().mockImplementation(() => ({
where: vi.fn().mockImplementation(() => {
@@ -1068,7 +1058,6 @@ export function createMockDatabase(options: MockDatabaseOptions = {}) {
})),
})
// Create delete chain
const createDeleteChain = () => ({
where: vi.fn().mockImplementation(() => {
if (deleteOptions.throwError) {
@@ -1078,7 +1067,6 @@ export function createMockDatabase(options: MockDatabaseOptions = {}) {
}),
})
// Create transaction mock
const createTransactionMock = () => {
return vi.fn().mockImplementation(async (callback: any) => {
if (transactionOptions.throwError) {
@@ -1200,7 +1188,6 @@ export function setupKnowledgeMocks(
mocks.generateEmbedding = vi.fn().mockResolvedValue([0.1, 0.2, 0.3])
}
// Mock the knowledge utilities
vi.doMock('@/app/api/knowledge/utils', () => mocks)
return mocks
@@ -1218,12 +1205,10 @@ export function setupFileApiMocks(
) {
const { authenticated = true, storageProvider = 's3', cloudEnabled = true } = options
// Setup basic mocks
setupCommonApiMocks()
mockUuid()
mockCryptoUuid()
// Setup auth
const authMocks = mockAuth()
if (authenticated) {
authMocks.setAuthenticated()
@@ -1231,14 +1216,12 @@ export function setupFileApiMocks(
authMocks.setUnauthenticated()
}
// Setup file system mocks
mockFileSystem({
writeFileSuccess: true,
readFileContent: 'test content',
existsResult: true,
})
// Setup storage provider mocks (this will mock @/lib/uploads)
let storageMocks
if (storageProvider) {
storageMocks = createStorageProviderMocks({
@@ -1246,7 +1229,6 @@ export function setupFileApiMocks(
isCloudEnabled: cloudEnabled,
})
} else {
// If no storage provider specified, just mock the base functions
vi.doMock('@/lib/uploads', () => ({
getStorageProvider: vi.fn().mockReturnValue('local'),
isUsingCloudStorage: vi.fn().mockReturnValue(cloudEnabled),
@@ -3,6 +3,7 @@ import { jwtDecode } from 'jwt-decode'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { db } from '@/db'
import { account, user } from '@/db/schema'
@@ -18,7 +19,7 @@ interface GoogleIdToken {
* Get all OAuth connections for the current user
*/
export async function GET(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
// Get the session
@@ -6,6 +6,7 @@ import { createLogger } from '@/lib/logs/console/logger'
import type { OAuthService } from '@/lib/oauth/oauth'
import { parseProvider } from '@/lib/oauth/oauth'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { generateRequestId } from '@/lib/utils'
import { db } from '@/db'
import { account, user, workflow } from '@/db/schema'
@@ -23,7 +24,7 @@ interface GoogleIdToken {
* Get credentials for a specific provider
*/
export async function GET(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
// Get query params
@@ -2,6 +2,7 @@ import { and, eq, like, or } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { db } from '@/db'
import { account } from '@/db/schema'
@@ -13,7 +14,7 @@ const logger = createLogger('OAuthDisconnectAPI')
* Disconnect an OAuth provider for the current user
*/
export async function POST(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
// Get the session
@@ -2,6 +2,7 @@ import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { db } from '@/db'
import { account } from '@/db/schema'
@@ -14,7 +15,7 @@ const logger = createLogger('MicrosoftFileAPI')
* Get a single file from Microsoft OneDrive
*/
export async function GET(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
// Get the session
const session = await getSession()
@@ -2,6 +2,7 @@ import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { db } from '@/db'
import { account } from '@/db/schema'
@@ -14,7 +15,7 @@ const logger = createLogger('MicrosoftFilesAPI')
* Get Excel files from Microsoft OneDrive
*/
export async function GET(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8) // Generate a short request ID for correlation
const requestId = generateRequestId()
try {
// Get the session
+3 -2
View File
@@ -2,6 +2,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { getCredential, refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
export const dynamic = 'force-dynamic'
@@ -14,7 +15,7 @@ const logger = createLogger('OAuthTokenAPI')
* and workflow-based authentication (for server-side requests)
*/
export async function POST(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
logger.info(`[${requestId}] OAuth token API POST request received`)
@@ -59,7 +60,7 @@ export async function POST(request: NextRequest) {
* Get the access token for a specific credential
*/
export async function GET(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8) // Short request ID for correlation
const requestId = generateRequestId()
try {
// Get the credential ID from the query params
@@ -2,6 +2,7 @@ import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { db } from '@/db'
import { account } from '@/db/schema'
@@ -14,7 +15,7 @@ const logger = createLogger('WealthboxItemAPI')
* Get a single item (note, contact, task) from Wealthbox
*/
export async function GET(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
// Get the session
@@ -2,6 +2,7 @@ import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { db } from '@/db'
import { account } from '@/db/schema'
@@ -14,7 +15,7 @@ const logger = createLogger('WealthboxItemsAPI')
* Get items (notes, contacts, tasks) from Wealthbox
*/
export async function GET(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
// Get the session
@@ -1,10 +1,10 @@
import crypto from 'crypto'
import { eq, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalApiKey } from '@/lib/copilot/utils'
import { isBillingEnabled } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { db } from '@/db'
import { userStats } from '@/db/schema'
import { calculateCost } from '@/providers/utils'
@@ -25,7 +25,7 @@ const UpdateCostSchema = z.object({
* Update user cost based on token usage with internal API key auth
*/
export async function POST(req: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const startTime = Date.now()
try {
@@ -5,6 +5,7 @@ import { renderOTPEmail } from '@/components/emails/render-email'
import { sendEmail } from '@/lib/email/mailer'
import { createLogger } from '@/lib/logs/console/logger'
import { getRedisClient, markMessageAsProcessed, releaseLock } from '@/lib/redis'
import { generateRequestId } from '@/lib/utils'
import { addCorsHeaders, setChatAuthCookie } from '@/app/api/chat/utils'
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
import { db } from '@/db'
@@ -115,7 +116,7 @@ export async function POST(
{ params }: { params: Promise<{ subdomain: string }> }
) {
const { subdomain } = await params
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
logger.debug(`[${requestId}] Processing OTP request for subdomain: ${subdomain}`)
@@ -229,7 +230,7 @@ export async function PUT(
{ params }: { params: Promise<{ subdomain: string }> }
) {
const { subdomain } = await params
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
logger.debug(`[${requestId}] Verifying OTP for subdomain: ${subdomain}`)
+3 -2
View File
@@ -1,6 +1,7 @@
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import {
addCorsHeaders,
executeWorkflowForChat,
@@ -20,7 +21,7 @@ export async function POST(
{ params }: { params: Promise<{ subdomain: string }> }
) {
const { subdomain } = await params
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
logger.debug(`[${requestId}] Processing chat request for subdomain: ${subdomain}`)
@@ -141,7 +142,7 @@ export async function GET(
{ params }: { params: Promise<{ subdomain: string }> }
) {
const { subdomain } = await params
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
logger.debug(`[${requestId}] Fetching chat info for subdomain: ${subdomain}`)
+11 -12
View File
@@ -14,10 +14,6 @@ vi.mock('@/db', () => ({
},
}))
vi.mock('@/lib/utils', () => ({
decryptSecret: vi.fn().mockResolvedValue({ decrypted: 'test-secret' }),
}))
vi.mock('@/lib/logs/execution/logging-session', () => ({
LoggingSession: vi.fn().mockImplementation(() => ({
safeStart: vi.fn().mockResolvedValue(undefined),
@@ -38,6 +34,13 @@ vi.mock('@/stores/workflows/server-utils', () => ({
mergeSubblockState: vi.fn().mockReturnValue({}),
}))
const mockDecryptSecret = vi.fn()
vi.mock('@/lib/utils', () => ({
decryptSecret: mockDecryptSecret,
generateRequestId: vi.fn(),
}))
describe('Chat API Utils', () => {
beforeEach(() => {
vi.resetModules()
@@ -177,7 +180,10 @@ describe('Chat API Utils', () => {
})
describe('Chat auth validation', () => {
beforeEach(() => {
beforeEach(async () => {
vi.clearAllMocks()
mockDecryptSecret.mockResolvedValue({ decrypted: 'correct-password' })
vi.doMock('@/app/api/chat/utils', async (importOriginal) => {
const original = (await importOriginal()) as any
return {
@@ -190,13 +196,6 @@ describe('Chat API Utils', () => {
}),
}
})
// Mock decryptSecret globally for all auth tests
vi.doMock('@/lib/utils', () => ({
decryptSecret: vi.fn((encryptedValue) => {
return Promise.resolve({ decrypted: 'correct-password' })
}),
}))
})
it.concurrent('should allow access to public chats', async () => {
+2 -2
View File
@@ -10,7 +10,7 @@ import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
import { hasAdminPermission } from '@/lib/permissions/utils'
import { processStreamingBlockLogs } from '@/lib/tokenization'
import { getEmailDomain } from '@/lib/urls/utils'
import { decryptSecret } from '@/lib/utils'
import { decryptSecret, generateRequestId } from '@/lib/utils'
import { getBlock } from '@/blocks'
import { db } from '@/db'
import { chat, userStats, workflow } from '@/db/schema'
@@ -303,7 +303,7 @@ export async function executeWorkflowForChat(
input: string,
conversationId?: string
): Promise<any> {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
logger.debug(
`[${requestId}] Executing workflow for chat: ${chatId}${
@@ -99,6 +99,7 @@ describe('Copilot Chat API Route', () => {
vi.doMock('@/lib/utils', () => ({
getRotatingApiKey: mockGetRotatingApiKey,
generateRequestId: vi.fn(() => 'test-request-id'),
}))
vi.doMock('@/lib/env', () => ({
+3 -3
View File
@@ -3,7 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { decryptSecret, encryptSecret } from '@/lib/utils'
import { decryptSecret, encryptSecret, generateRequestId } from '@/lib/utils'
import { db } from '@/db'
import { environment } from '@/db/schema'
import type { EnvironmentVariable } from '@/stores/settings/environment/types'
@@ -15,7 +15,7 @@ const EnvVarSchema = z.object({
})
export async function POST(req: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const session = await getSession()
@@ -72,7 +72,7 @@ export async function POST(req: NextRequest) {
}
export async function GET(request: Request) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const session = await getSession()
+2 -2
View File
@@ -4,7 +4,7 @@ import { env, isTruthy } from '@/lib/env'
import { executeInE2B } from '@/lib/execution/e2b'
import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
export const dynamic = 'force-dynamic'
export const runtime = 'nodejs'
export const maxDuration = 60
@@ -533,7 +533,7 @@ function escapeRegExp(string: string): string {
}
export async function POST(req: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const startTime = Date.now()
let stdout = ''
let userCodeStartLine = 3 // Default value for error reporting
+2 -1
View File
@@ -7,6 +7,7 @@ import { getFromEmailAddress } from '@/lib/email/utils'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { getEmailDomain } from '@/lib/urls/utils'
import { generateRequestId } from '@/lib/utils'
const logger = createLogger('HelpAPI')
@@ -17,7 +18,7 @@ const helpFormSchema = z.object({
})
export async function POST(req: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
// Get user session
+2 -1
View File
@@ -3,6 +3,7 @@ import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { createErrorResponse } from '@/app/api/workflows/utils'
import { db } from '@/db'
import { apiKey as apiKeyTable } from '@/db/schema'
@@ -14,7 +15,7 @@ export async function GET(
{ params }: { params: Promise<{ jobId: string }> }
) {
const { jobId: taskId } = await params
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
logger.debug(`[${requestId}] Getting status for task: ${taskId}`)
@@ -1,9 +1,9 @@
import crypto from 'crypto'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { batchChunkOperation, createChunk, queryChunks } from '@/lib/knowledge/chunks/service'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { getUserId } from '@/app/api/auth/oauth/utils'
import { checkDocumentAccess, checkDocumentWriteAccess } from '@/app/api/knowledge/utils'
import { calculateCost } from '@/providers/utils'
@@ -34,7 +34,7 @@ export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ id: string; documentId: string }> }
) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const { id: knowledgeBaseId, documentId } = await params
try {
@@ -106,7 +106,7 @@ export async function POST(
req: NextRequest,
{ params }: { params: Promise<{ id: string; documentId: string }> }
) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const { id: knowledgeBaseId, documentId } = await params
try {
@@ -229,7 +229,7 @@ export async function PATCH(
req: NextRequest,
{ params }: { params: Promise<{ id: string; documentId: string }> }
) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const { id: knowledgeBaseId, documentId } = await params
try {
@@ -8,6 +8,7 @@ import {
updateDocument,
} from '@/lib/knowledge/documents/service'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { checkDocumentAccess, checkDocumentWriteAccess } from '@/app/api/knowledge/utils'
const logger = createLogger('DocumentByIdAPI')
@@ -36,7 +37,7 @@ export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ id: string; documentId: string }> }
) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const { id: knowledgeBaseId, documentId } = await params
try {
@@ -79,7 +80,7 @@ export async function PUT(
req: NextRequest,
{ params }: { params: Promise<{ id: string; documentId: string }> }
) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const { id: knowledgeBaseId, documentId } = await params
try {
@@ -209,7 +210,7 @@ export async function DELETE(
req: NextRequest,
{ params }: { params: Promise<{ id: string; documentId: string }> }
) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const { id: knowledgeBaseId, documentId } = await params
try {
+4 -3
View File
@@ -7,6 +7,7 @@ import {
updateKnowledgeBase,
} from '@/lib/knowledge/service'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { checkKnowledgeBaseAccess, checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils'
const logger = createLogger('KnowledgeBaseByIdAPI')
@@ -27,7 +28,7 @@ const UpdateKnowledgeBaseSchema = z.object({
})
export async function GET(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const { id } = await params
try {
@@ -69,7 +70,7 @@ export async function GET(_req: NextRequest, { params }: { params: Promise<{ id:
}
export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const { id } = await params
try {
@@ -132,7 +133,7 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id:
}
export async function DELETE(_req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const { id } = await params
try {
+3 -2
View File
@@ -3,6 +3,7 @@ import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createKnowledgeBase, getKnowledgeBases } from '@/lib/knowledge/service'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
const logger = createLogger('KnowledgeBaseAPI')
@@ -29,7 +30,7 @@ const CreateKnowledgeBaseSchema = z.object({
})
export async function GET(req: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const session = await getSession()
@@ -54,7 +55,7 @@ export async function GET(req: NextRequest) {
}
export async function POST(req: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const session = await getSession()
@@ -34,6 +34,10 @@ vi.mock('@/lib/env', () => ({
typeof value === 'string' ? value === 'true' || value === '1' : Boolean(value),
}))
vi.mock('@/lib/utils', () => ({
generateRequestId: vi.fn(() => 'test-request-id'),
}))
vi.mock('@/lib/documents/utils', () => ({
retryWithExponentialBackoff: vi.fn().mockImplementation((fn) => fn()),
}))
+2 -1
View File
@@ -4,6 +4,7 @@ import { TAG_SLOTS } from '@/lib/knowledge/consts'
import { getDocumentTagDefinitions } from '@/lib/knowledge/tags/service'
import { createLogger } from '@/lib/logs/console/logger'
import { estimateTokenCount } from '@/lib/tokenization/estimators'
import { generateRequestId } from '@/lib/utils'
import { getUserId } from '@/app/api/auth/oauth/utils'
import { checkKnowledgeBaseAccess } from '@/app/api/knowledge/utils'
import { calculateCost } from '@/providers/utils'
@@ -57,7 +58,7 @@ const VectorSearchSchema = z
)
export async function POST(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const body = await request.json()
+2 -1
View File
@@ -2,6 +2,7 @@ import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { db } from '@/db'
import { permissions, workflow, workflowExecutionLogs } from '@/db/schema'
@@ -10,7 +11,7 @@ const logger = createLogger('LogDetailsByIdAPI')
export const revalidate = 0
export async function GET(_request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const session = await getSession()
+2 -34
View File
@@ -3,44 +3,12 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { db } from '@/db'
import { permissions, workflow, workflowExecutionLogs } from '@/db/schema'
const logger = createLogger('LogsAPI')
// Helper function to extract block executions from trace spans
function extractBlockExecutionsFromTraceSpans(traceSpans: any[]): any[] {
const blockExecutions: any[] = []
function processSpan(span: any) {
if (span.blockId) {
blockExecutions.push({
id: span.id,
blockId: span.blockId,
blockName: span.name || '',
blockType: span.type,
startedAt: span.startTime,
endedAt: span.endTime,
durationMs: span.duration || 0,
status: span.status || 'success',
errorMessage: span.output?.error || undefined,
inputData: span.input || {},
outputData: span.output || {},
cost: span.cost || undefined,
metadata: {},
})
}
// Process children recursively
if (span.children && Array.isArray(span.children)) {
span.children.forEach(processSpan)
}
}
traceSpans.forEach(processSpan)
return blockExecutions
}
export const revalidate = 0
const QueryParamsSchema = z.object({
@@ -58,7 +26,7 @@ const QueryParamsSchema = z.object({
})
export async function GET(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const session = await getSession()
@@ -0,0 +1,99 @@
import { and, eq, isNull } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { withMcpAuth } from '@/lib/mcp/middleware'
import { mcpService } from '@/lib/mcp/service'
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
import { db } from '@/db'
import { mcpServers } from '@/db/schema'
const logger = createLogger('McpServerRefreshAPI')
export const dynamic = 'force-dynamic'
/**
* POST - Refresh an MCP server connection (requires any workspace permission)
*/
export const POST = withMcpAuth('read')(
async (
request: NextRequest,
{ userId, workspaceId, requestId },
{ params }: { params: { id: string } }
) => {
const serverId = params.id
try {
logger.info(
`[${requestId}] Refreshing MCP server: ${serverId} in workspace: ${workspaceId}`,
{
userId,
}
)
const [server] = await db
.select()
.from(mcpServers)
.where(
and(
eq(mcpServers.id, serverId),
eq(mcpServers.workspaceId, workspaceId),
isNull(mcpServers.deletedAt)
)
)
.limit(1)
if (!server) {
return createMcpErrorResponse(
new Error('Server not found or access denied'),
'Server not found',
404
)
}
let connectionStatus: 'connected' | 'disconnected' | 'error' = 'error'
let toolCount = 0
let lastError: string | null = null
try {
const tools = await mcpService.discoverServerTools(userId, serverId, workspaceId)
connectionStatus = 'connected'
toolCount = tools.length
logger.info(
`[${requestId}] Successfully connected to server ${serverId}, discovered ${toolCount} tools`
)
} catch (error) {
connectionStatus = 'error'
lastError = error instanceof Error ? error.message : 'Connection test failed'
logger.warn(`[${requestId}] Failed to connect to server ${serverId}:`, error)
}
const [refreshedServer] = await db
.update(mcpServers)
.set({
lastToolsRefresh: new Date(),
connectionStatus,
lastError,
lastConnected: connectionStatus === 'connected' ? new Date() : server.lastConnected,
toolCount,
updatedAt: new Date(),
})
.where(eq(mcpServers.id, serverId))
.returning()
logger.info(`[${requestId}] Successfully refreshed MCP server: ${serverId}`)
return createMcpSuccessResponse({
status: connectionStatus,
toolCount,
lastConnected: refreshedServer?.lastConnected?.toISOString() || null,
error: lastError,
})
} catch (error) {
logger.error(`[${requestId}] Error refreshing MCP server:`, error)
return createMcpErrorResponse(
error instanceof Error ? error : new Error('Failed to refresh MCP server'),
'Failed to refresh MCP server',
500
)
}
}
)
@@ -0,0 +1,92 @@
import { and, eq, isNull } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
import { mcpService } from '@/lib/mcp/service'
import { validateMcpServerUrl } from '@/lib/mcp/url-validator'
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
import { db } from '@/db'
import { mcpServers } from '@/db/schema'
const logger = createLogger('McpServerAPI')
export const dynamic = 'force-dynamic'
/**
* PATCH - Update an MCP server in the workspace (requires write or admin permission)
*/
export const PATCH = withMcpAuth('write')(
async (
request: NextRequest,
{ userId, workspaceId, requestId },
{ params }: { params: { id: string } }
) => {
const serverId = params.id
try {
const body = getParsedBody(request) || (await request.json())
logger.info(`[${requestId}] Updating MCP server: ${serverId} in workspace: ${workspaceId}`, {
userId,
updates: Object.keys(body).filter((k) => k !== 'workspaceId'),
})
// Validate URL if being updated
if (
body.url &&
(body.transport === 'http' ||
body.transport === 'sse' ||
body.transport === 'streamable-http')
) {
const urlValidation = validateMcpServerUrl(body.url)
if (!urlValidation.isValid) {
return createMcpErrorResponse(
new Error(`Invalid MCP server URL: ${urlValidation.error}`),
'Invalid server URL',
400
)
}
body.url = urlValidation.normalizedUrl
}
// Remove workspaceId from body to prevent it from being updated
const { workspaceId: _, ...updateData } = body
const [updatedServer] = await db
.update(mcpServers)
.set({
...updateData,
updatedAt: new Date(),
})
.where(
and(
eq(mcpServers.id, serverId),
eq(mcpServers.workspaceId, workspaceId),
isNull(mcpServers.deletedAt)
)
)
.returning()
if (!updatedServer) {
return createMcpErrorResponse(
new Error('Server not found or access denied'),
'Server not found',
404
)
}
// Clear MCP service cache after update
mcpService.clearCache(workspaceId)
logger.info(`[${requestId}] Successfully updated MCP server: ${serverId}`)
return createMcpSuccessResponse({ server: updatedServer })
} catch (error) {
logger.error(`[${requestId}] Error updating MCP server:`, error)
return createMcpErrorResponse(
error instanceof Error ? error : new Error('Failed to update MCP server'),
'Failed to update MCP server',
500
)
}
}
)
+166
View File
@@ -0,0 +1,166 @@
import { and, eq, isNull } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
import { mcpService } from '@/lib/mcp/service'
import type { McpTransport } from '@/lib/mcp/types'
import { validateMcpServerUrl } from '@/lib/mcp/url-validator'
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
import { db } from '@/db'
import { mcpServers } from '@/db/schema'
const logger = createLogger('McpServersAPI')
export const dynamic = 'force-dynamic'
/**
* Check if transport type requires a URL
*/
function isUrlBasedTransport(transport: McpTransport): boolean {
return transport === 'http' || transport === 'sse' || transport === 'streamable-http'
}
/**
* GET - List all registered MCP servers for the workspace
*/
export const GET = withMcpAuth('read')(
async (request: NextRequest, { userId, workspaceId, requestId }) => {
try {
logger.info(`[${requestId}] Listing MCP servers for workspace ${workspaceId}`)
const servers = await db
.select()
.from(mcpServers)
.where(and(eq(mcpServers.workspaceId, workspaceId), isNull(mcpServers.deletedAt)))
logger.info(
`[${requestId}] Listed ${servers.length} MCP servers for workspace ${workspaceId}`
)
return createMcpSuccessResponse({ servers })
} catch (error) {
logger.error(`[${requestId}] Error listing MCP servers:`, error)
return createMcpErrorResponse(
error instanceof Error ? error : new Error('Failed to list MCP servers'),
'Failed to list MCP servers',
500
)
}
}
)
/**
* POST - Register a new MCP server for the workspace (requires write permission)
*/
export const POST = withMcpAuth('write')(
async (request: NextRequest, { userId, workspaceId, requestId }) => {
try {
const body = getParsedBody(request) || (await request.json())
logger.info(`[${requestId}] Registering new MCP server:`, {
name: body.name,
transport: body.transport,
workspaceId,
})
if (!body.name || !body.transport) {
return createMcpErrorResponse(
new Error('Missing required fields: name or transport'),
'Missing required fields',
400
)
}
if (isUrlBasedTransport(body.transport) && body.url) {
const urlValidation = validateMcpServerUrl(body.url)
if (!urlValidation.isValid) {
return createMcpErrorResponse(
new Error(`Invalid MCP server URL: ${urlValidation.error}`),
'Invalid server URL',
400
)
}
body.url = urlValidation.normalizedUrl
}
const serverId = body.id || crypto.randomUUID()
await db
.insert(mcpServers)
.values({
id: serverId,
workspaceId,
createdBy: userId,
name: body.name,
description: body.description,
transport: body.transport,
url: body.url,
headers: body.headers || {},
timeout: body.timeout || 30000,
retries: body.retries || 3,
enabled: body.enabled !== false,
createdAt: new Date(),
updatedAt: new Date(),
})
.returning()
mcpService.clearCache(workspaceId)
logger.info(`[${requestId}] Successfully registered MCP server: ${body.name}`)
return createMcpSuccessResponse({ serverId }, 201)
} catch (error) {
logger.error(`[${requestId}] Error registering MCP server:`, error)
return createMcpErrorResponse(
error instanceof Error ? error : new Error('Failed to register MCP server'),
'Failed to register MCP server',
500
)
}
}
)
/**
* DELETE - Delete an MCP server from the workspace (requires admin permission)
*/
export const DELETE = withMcpAuth('admin')(
async (request: NextRequest, { userId, workspaceId, requestId }) => {
try {
const { searchParams } = new URL(request.url)
const serverId = searchParams.get('serverId')
if (!serverId) {
return createMcpErrorResponse(
new Error('serverId parameter is required'),
'Missing required parameter',
400
)
}
logger.info(`[${requestId}] Deleting MCP server: ${serverId} from workspace: ${workspaceId}`)
const [deletedServer] = await db
.delete(mcpServers)
.where(and(eq(mcpServers.id, serverId), eq(mcpServers.workspaceId, workspaceId)))
.returning()
if (!deletedServer) {
return createMcpErrorResponse(
new Error('Server not found or access denied'),
'Server not found',
404
)
}
mcpService.clearCache(workspaceId)
logger.info(`[${requestId}] Successfully deleted MCP server: ${serverId}`)
return createMcpSuccessResponse({ message: `Server ${serverId} deleted successfully` })
} catch (error) {
logger.error(`[${requestId}] Error deleting MCP server:`, error)
return createMcpErrorResponse(
error instanceof Error ? error : new Error('Failed to delete MCP server'),
'Failed to delete MCP server',
500
)
}
}
)
@@ -0,0 +1,209 @@
import type { NextRequest } from 'next/server'
import { getEffectiveDecryptedEnv } from '@/lib/environment/utils'
import { createLogger } from '@/lib/logs/console/logger'
import { McpClient } from '@/lib/mcp/client'
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
import type { McpServerConfig, McpTransport } from '@/lib/mcp/types'
import { validateMcpServerUrl } from '@/lib/mcp/url-validator'
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
const logger = createLogger('McpServerTestAPI')
export const dynamic = 'force-dynamic'
/**
* Check if transport type requires a URL
*/
function isUrlBasedTransport(transport: McpTransport): boolean {
return transport === 'http' || transport === 'sse' || transport === 'streamable-http'
}
/**
* Resolve environment variables in strings
*/
function resolveEnvVars(value: string, envVars: Record<string, string>): string {
const envMatches = value.match(/\{\{([^}]+)\}\}/g)
if (!envMatches) return value
let resolvedValue = value
for (const match of envMatches) {
const envKey = match.slice(2, -2).trim()
const envValue = envVars[envKey]
if (envValue === undefined) {
logger.warn(`Environment variable "${envKey}" not found in MCP server test`)
continue
}
resolvedValue = resolvedValue.replace(match, envValue)
}
return resolvedValue
}
interface TestConnectionRequest {
name: string
transport: McpTransport
url?: string
headers?: Record<string, string>
timeout?: number
workspaceId: string
}
interface TestConnectionResult {
success: boolean
error?: string
serverInfo?: {
name: string
version: string
}
negotiatedVersion?: string
supportedCapabilities?: string[]
toolCount?: number
warnings?: string[]
}
/**
* POST - Test connection to an MCP server before registering it
*/
export const POST = withMcpAuth('write')(
async (request: NextRequest, { userId, workspaceId, requestId }) => {
try {
const body: TestConnectionRequest = getParsedBody(request) || (await request.json())
logger.info(`[${requestId}] Testing MCP server connection:`, {
name: body.name,
transport: body.transport,
url: body.url ? `${body.url.substring(0, 50)}...` : undefined, // Partial URL for security
workspaceId,
})
if (!body.name || !body.transport) {
return createMcpErrorResponse(
new Error('Missing required fields: name and transport are required'),
'Missing required fields',
400
)
}
if (isUrlBasedTransport(body.transport)) {
if (!body.url) {
return createMcpErrorResponse(
new Error('URL is required for HTTP-based transports'),
'Missing required URL',
400
)
}
const urlValidation = validateMcpServerUrl(body.url)
if (!urlValidation.isValid) {
return createMcpErrorResponse(
new Error(`Invalid MCP server URL: ${urlValidation.error}`),
'Invalid server URL',
400
)
}
body.url = urlValidation.normalizedUrl
}
let resolvedUrl = body.url
let resolvedHeaders = body.headers || {}
try {
const envVars = await getEffectiveDecryptedEnv(userId, workspaceId)
if (resolvedUrl) {
resolvedUrl = resolveEnvVars(resolvedUrl, envVars)
}
const resolvedHeadersObj: Record<string, string> = {}
for (const [key, value] of Object.entries(resolvedHeaders)) {
resolvedHeadersObj[key] = resolveEnvVars(value, envVars)
}
resolvedHeaders = resolvedHeadersObj
} catch (envError) {
logger.warn(
`[${requestId}] Failed to resolve environment variables, using raw values:`,
envError
)
}
const testConfig: McpServerConfig = {
id: `test-${requestId}`,
name: body.name,
transport: body.transport,
url: resolvedUrl,
headers: resolvedHeaders,
timeout: body.timeout || 10000,
retries: 1, // Only one retry for tests
enabled: true,
}
const testSecurityPolicy = {
requireConsent: false,
auditLevel: 'none' as const,
maxToolExecutionsPerHour: 0,
}
const result: TestConnectionResult = { success: false }
let client: McpClient | null = null
try {
client = new McpClient(testConfig, testSecurityPolicy)
await client.connect()
result.success = true
result.negotiatedVersion = client.getNegotiatedVersion()
try {
const tools = await client.listTools()
result.toolCount = tools.length
} catch (toolError) {
logger.warn(`[${requestId}] Could not list tools from test server:`, toolError)
result.warnings = result.warnings || []
result.warnings.push('Could not list tools from server')
}
const clientVersionInfo = McpClient.getVersionInfo()
if (result.negotiatedVersion !== clientVersionInfo.preferred) {
result.warnings = result.warnings || []
result.warnings.push(
`Server uses protocol version '${result.negotiatedVersion}' instead of preferred '${clientVersionInfo.preferred}'`
)
}
logger.info(`[${requestId}] MCP server test successful:`, {
name: body.name,
negotiatedVersion: result.negotiatedVersion,
toolCount: result.toolCount,
capabilities: result.supportedCapabilities,
})
} catch (error) {
logger.warn(`[${requestId}] MCP server test failed:`, error)
result.success = false
if (error instanceof Error) {
result.error = error.message
} else {
result.error = 'Unknown connection error'
}
} finally {
if (client) {
try {
await client.disconnect()
} catch (disconnectError) {
logger.debug(`[${requestId}] Test client disconnect error (expected):`, disconnectError)
}
}
}
return createMcpSuccessResponse(result, result.success ? 200 : 400)
} catch (error) {
logger.error(`[${requestId}] Error testing MCP server connection:`, error)
return createMcpErrorResponse(
error instanceof Error ? error : new Error('Failed to test server connection'),
'Failed to test server connection',
500
)
}
}
)
@@ -0,0 +1,122 @@
import type { NextRequest } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
import { mcpService } from '@/lib/mcp/service'
import type { McpToolDiscoveryResponse } from '@/lib/mcp/types'
import { categorizeError, createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
const logger = createLogger('McpToolDiscoveryAPI')
export const dynamic = 'force-dynamic'
/**
* GET - Discover all tools from user's MCP servers
*/
export const GET = withMcpAuth('read')(
async (request: NextRequest, { userId, workspaceId, requestId }) => {
try {
const { searchParams } = new URL(request.url)
const serverId = searchParams.get('serverId')
const forceRefresh = searchParams.get('refresh') === 'true'
logger.info(`[${requestId}] Discovering MCP tools for user ${userId}`, {
serverId,
workspaceId,
forceRefresh,
})
let tools
if (serverId) {
tools = await mcpService.discoverServerTools(userId, serverId, workspaceId)
} else {
tools = await mcpService.discoverTools(userId, workspaceId, forceRefresh)
}
const byServer: Record<string, number> = {}
for (const tool of tools) {
byServer[tool.serverId] = (byServer[tool.serverId] || 0) + 1
}
const responseData: McpToolDiscoveryResponse = {
tools,
totalCount: tools.length,
byServer,
}
logger.info(
`[${requestId}] Discovered ${tools.length} tools from ${Object.keys(byServer).length} servers`
)
return createMcpSuccessResponse(responseData)
} catch (error) {
logger.error(`[${requestId}] Error discovering MCP tools:`, error)
const { message, status } = categorizeError(error)
return createMcpErrorResponse(new Error(message), 'Failed to discover MCP tools', status)
}
}
)
/**
* POST - Refresh tool discovery for specific servers
*/
export const POST = withMcpAuth('read')(
async (request: NextRequest, { userId, workspaceId, requestId }) => {
try {
const body = getParsedBody(request) || (await request.json())
const { serverIds } = body
if (!Array.isArray(serverIds)) {
return createMcpErrorResponse(
new Error('serverIds must be an array'),
'Invalid request format',
400
)
}
logger.info(
`[${requestId}] Refreshing tool discovery for user ${userId}, servers:`,
serverIds
)
const results = await Promise.allSettled(
serverIds.map(async (serverId: string) => {
const tools = await mcpService.discoverServerTools(userId, serverId, workspaceId)
return { serverId, toolCount: tools.length }
})
)
const successes: Array<{ serverId: string; toolCount: number }> = []
const failures: Array<{ serverId: string; error: string }> = []
results.forEach((result, index) => {
const serverId = serverIds[index]
if (result.status === 'fulfilled') {
successes.push(result.value)
} else {
failures.push({
serverId,
error: result.reason instanceof Error ? result.reason.message : 'Unknown error',
})
}
})
const responseData = {
refreshed: successes,
failed: failures,
summary: {
total: serverIds.length,
successful: successes.length,
failed: failures.length,
},
}
logger.info(
`[${requestId}] Tool discovery refresh completed: ${successes.length}/${serverIds.length} successful`
)
return createMcpSuccessResponse(responseData)
} catch (error) {
logger.error(`[${requestId}] Error refreshing tool discovery:`, error)
const { message, status } = categorizeError(error)
return createMcpErrorResponse(new Error(message), 'Failed to refresh tool discovery', status)
}
}
)
+252
View File
@@ -0,0 +1,252 @@
import type { NextRequest } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
import { mcpService } from '@/lib/mcp/service'
import type { McpTool, McpToolCall, McpToolResult } from '@/lib/mcp/types'
import {
categorizeError,
createMcpErrorResponse,
createMcpSuccessResponse,
MCP_CONSTANTS,
validateStringParam,
} from '@/lib/mcp/utils'
const logger = createLogger('McpToolExecutionAPI')
export const dynamic = 'force-dynamic'
// Type definitions for improved type safety
interface SchemaProperty {
type: 'string' | 'number' | 'boolean' | 'object' | 'array'
description?: string
enum?: unknown[]
format?: string
items?: SchemaProperty
properties?: Record<string, SchemaProperty>
}
interface ToolExecutionResult {
success: boolean
output?: McpToolResult
error?: string
}
/**
* Type guard to safely check if a schema property has a type field
*/
function hasType(prop: unknown): prop is SchemaProperty {
return typeof prop === 'object' && prop !== null && 'type' in prop
}
/**
* POST - Execute a tool on an MCP server
*/
export const POST = withMcpAuth('read')(
async (request: NextRequest, { userId, workspaceId, requestId }) => {
try {
const body = getParsedBody(request) || (await request.json())
logger.info(`[${requestId}] MCP tool execution request received`, {
hasAuthHeader: !!request.headers.get('authorization'),
authHeaderType: request.headers.get('authorization')?.substring(0, 10),
bodyKeys: Object.keys(body),
serverId: body.serverId,
toolName: body.toolName,
hasWorkflowId: !!body.workflowId,
workflowId: body.workflowId,
userId: userId,
})
const { serverId, toolName, arguments: args } = body
const serverIdValidation = validateStringParam(serverId, 'serverId')
if (!serverIdValidation.isValid) {
logger.warn(`[${requestId}] Invalid serverId: ${serverId}`)
return createMcpErrorResponse(new Error(serverIdValidation.error), 'Invalid serverId', 400)
}
const toolNameValidation = validateStringParam(toolName, 'toolName')
if (!toolNameValidation.isValid) {
logger.warn(`[${requestId}] Invalid toolName: ${toolName}`)
return createMcpErrorResponse(new Error(toolNameValidation.error), 'Invalid toolName', 400)
}
logger.info(
`[${requestId}] Executing tool ${toolName} on server ${serverId} for user ${userId} in workspace ${workspaceId}`
)
let tool = null
try {
const tools = await mcpService.discoverServerTools(userId, serverId, workspaceId)
tool = tools.find((t) => t.name === toolName)
if (!tool) {
return createMcpErrorResponse(
new Error(
`Tool ${toolName} not found on server ${serverId}. Available tools: ${tools.map((t) => t.name).join(', ')}`
),
'Tool not found',
404
)
}
// Parse array arguments based on tool schema
if (tool.inputSchema?.properties) {
for (const [paramName, paramSchema] of Object.entries(tool.inputSchema.properties)) {
const schema = paramSchema as any
if (
schema.type === 'array' &&
args[paramName] !== undefined &&
typeof args[paramName] === 'string'
) {
const stringValue = args[paramName].trim()
if (stringValue) {
try {
// Try to parse as JSON first (handles ["item1", "item2"])
const parsed = JSON.parse(stringValue)
if (Array.isArray(parsed)) {
args[paramName] = parsed
} else {
// JSON parsed but not an array, wrap in array
args[paramName] = [parsed]
}
} catch (error) {
// JSON parsing failed - treat as comma-separated if contains commas, otherwise single item
if (stringValue.includes(',')) {
args[paramName] = stringValue
.split(',')
.map((item) => item.trim())
.filter((item) => item)
} else {
// Single item - wrap in array since schema expects array
args[paramName] = [stringValue]
}
}
} else {
// Empty string becomes empty array
args[paramName] = []
}
}
}
}
} catch (error) {
logger.warn(
`[${requestId}] Failed to discover tools for validation, proceeding anyway:`,
error
)
}
if (tool) {
const validationError = validateToolArguments(tool, args)
if (validationError) {
logger.warn(`[${requestId}] Tool validation failed: ${validationError}`)
return createMcpErrorResponse(
new Error(`Invalid arguments for tool ${toolName}: ${validationError}`),
'Invalid tool arguments',
400
)
}
}
const toolCall: McpToolCall = {
name: toolName,
arguments: args || {},
}
const result = await Promise.race([
mcpService.executeTool(userId, serverId, toolCall, workspaceId),
new Promise<never>((_, reject) =>
setTimeout(
() => reject(new Error('Tool execution timeout')),
MCP_CONSTANTS.EXECUTION_TIMEOUT
)
),
])
const transformedResult = transformToolResult(result)
if (result.isError) {
logger.warn(`[${requestId}] Tool execution returned error for ${toolName} on ${serverId}`)
return createMcpErrorResponse(
transformedResult,
transformedResult.error || 'Tool execution failed',
400
)
}
logger.info(`[${requestId}] Successfully executed tool ${toolName} on server ${serverId}`)
return createMcpSuccessResponse(transformedResult)
} catch (error) {
logger.error(`[${requestId}] Error executing MCP tool:`, error)
const { message, status } = categorizeError(error)
return createMcpErrorResponse(new Error(message), message, status)
}
}
)
/**
* Validate tool arguments against schema
*/
function validateToolArguments(tool: McpTool, args: Record<string, unknown>): string | null {
if (!tool.inputSchema) {
return null // No schema to validate against
}
const schema = tool.inputSchema
if (schema.required && Array.isArray(schema.required)) {
for (const requiredProp of schema.required) {
if (!(requiredProp in (args || {}))) {
return `Missing required property: ${requiredProp}`
}
}
}
if (schema.properties && args) {
for (const [propName, propSchema] of Object.entries(schema.properties)) {
const propValue = args[propName]
if (propValue !== undefined && hasType(propSchema)) {
const expectedType = propSchema.type
const actualType = typeof propValue
if (expectedType === 'string' && actualType !== 'string') {
return `Property ${propName} must be a string`
}
if (expectedType === 'number' && actualType !== 'number') {
return `Property ${propName} must be a number`
}
if (expectedType === 'boolean' && actualType !== 'boolean') {
return `Property ${propName} must be a boolean`
}
if (
expectedType === 'object' &&
(actualType !== 'object' || propValue === null || Array.isArray(propValue))
) {
return `Property ${propName} must be an object`
}
if (expectedType === 'array' && !Array.isArray(propValue)) {
return `Property ${propName} must be an array`
}
}
}
}
return null
}
/**
* Transform MCP tool result to platform format
*/
function transformToolResult(result: McpToolResult): ToolExecutionResult {
if (result.isError) {
return {
success: false,
error: result.content?.[0]?.text || 'Tool execution failed',
}
}
return {
success: true,
output: result,
}
}
+4 -3
View File
@@ -1,6 +1,7 @@
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { db } from '@/db'
import { memory } from '@/db/schema'
@@ -13,7 +14,7 @@ export const runtime = 'nodejs'
* GET handler for retrieving a specific memory by ID
*/
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const { id } = await params
try {
@@ -85,7 +86,7 @@ export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const { id } = await params
try {
@@ -156,7 +157,7 @@ export async function DELETE(
* PUT handler for updating a specific memory
*/
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const { id } = await params
try {
+3 -2
View File
@@ -1,6 +1,7 @@
import { and, eq, isNull, like } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { db } from '@/db'
import { memory } from '@/db/schema'
@@ -18,7 +19,7 @@ export const runtime = 'nodejs'
* - workflowId: Filter by workflow ID (required)
*/
export async function GET(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
logger.info(`[${requestId}] Processing memory search request`)
@@ -101,7 +102,7 @@ export async function GET(request: NextRequest) {
* - workflowId: ID of the workflow this memory belongs to
*/
export async function POST(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
logger.info(`[${requestId}] Processing memory creation request`)
+4 -1
View File
@@ -1,5 +1,6 @@
import { type NextRequest, NextResponse } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import type { StreamingExecution } from '@/executor/types'
import { executeProviderRequest } from '@/providers'
import { getApiKey } from '@/providers/utils'
@@ -12,7 +13,7 @@ export const dynamic = 'force-dynamic'
* Server-side proxy for provider requests
*/
export async function POST(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const startTime = Date.now()
try {
@@ -36,6 +37,7 @@ export async function POST(request: NextRequest) {
azureApiVersion,
responseFormat,
workflowId,
workspaceId,
stream,
messages,
environmentVariables,
@@ -104,6 +106,7 @@ export async function POST(request: NextRequest) {
azureApiVersion,
responseFormat,
workflowId,
workspaceId,
stream,
messages,
environmentVariables,
+2 -1
View File
@@ -1,6 +1,7 @@
import { type NextRequest, NextResponse } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { validateImageUrl } from '@/lib/security/url-validation'
import { generateRequestId } from '@/lib/utils'
const logger = createLogger('ImageProxyAPI')
@@ -11,7 +12,7 @@ const logger = createLogger('ImageProxyAPI')
export async function GET(request: NextRequest) {
const url = new URL(request.url)
const imageUrl = url.searchParams.get('url')
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
if (!imageUrl) {
logger.error(`[${requestId}] Missing 'url' parameter`)
+3 -2
View File
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'
import { isDev } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console/logger'
import { validateProxyUrl } from '@/lib/security/url-validation'
import { generateRequestId } from '@/lib/utils'
import { executeTool } from '@/tools'
import { getTool, validateRequiredParametersAfterMerge } from '@/tools/utils'
@@ -74,7 +75,7 @@ const createErrorResponse = (error: any, status = 500, additionalData = {}) => {
export async function GET(request: Request) {
const url = new URL(request.url)
const targetUrl = url.searchParams.get('url')
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
if (!targetUrl) {
logger.error(`[${requestId}] Missing 'url' parameter`)
@@ -167,7 +168,7 @@ export async function GET(request: Request) {
}
export async function POST(request: Request) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const startTime = new Date()
const startTimeISO = startTime.toISOString()
+3 -3
View File
@@ -1,9 +1,9 @@
import crypto from 'crypto'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { generateRequestId } from '@/lib/utils'
import { db } from '@/db'
import { workflow, workflowSchedule } from '@/db/schema'
@@ -18,7 +18,7 @@ export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const { id } = await params
@@ -85,7 +85,7 @@ export async function DELETE(
* Update a schedule - can be used to reactivate a disabled schedule
*/
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const { id } = await params
@@ -3,13 +3,14 @@ import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { generateRequestId } from '@/lib/utils'
import { db } from '@/db'
import { workflow, workflowSchedule } from '@/db/schema'
const logger = createLogger('ScheduleStatusAPI')
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const { id } = await params
const scheduleId = id
+2 -3
View File
@@ -15,7 +15,7 @@ import {
getScheduleTimeValues,
getSubBlockValue,
} from '@/lib/schedules/utils'
import { decryptSecret } from '@/lib/utils'
import { decryptSecret, generateRequestId } from '@/lib/utils'
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers'
import { updateWorkflowRunCounts } from '@/lib/workflows/utils'
import { db } from '@/db'
@@ -25,7 +25,6 @@ import { Serializer } from '@/serializer'
import { RateLimiter } from '@/services/queue'
import { mergeSubblockState } from '@/stores/workflows/server-utils'
// Add dynamic export to prevent caching
export const dynamic = 'force-dynamic'
const logger = createLogger('ScheduledExecuteAPI')
@@ -66,7 +65,7 @@ const runningExecutions = new Set<string>()
export async function GET() {
logger.info(`Scheduled execution triggered at ${new Date().toISOString()}`)
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const now = new Date()
let dueSchedules: (typeof workflowSchedule.$inferSelect)[] = []
+3 -3
View File
@@ -1,4 +1,3 @@
import crypto from 'crypto'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
@@ -13,6 +12,7 @@ import {
getSubBlockValue,
validateCronExpression,
} from '@/lib/schedules/utils'
import { generateRequestId } from '@/lib/utils'
import { db } from '@/db'
import { workflow, workflowSchedule } from '@/db/schema'
@@ -65,7 +65,7 @@ function hasValidScheduleConfig(
* Get schedule information for a workflow
*/
export async function GET(req: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const url = new URL(req.url)
const workflowId = url.searchParams.get('workflowId')
const blockId = url.searchParams.get('blockId')
@@ -165,7 +165,7 @@ export async function GET(req: NextRequest) {
* Create or update a schedule for a workflow
*/
export async function POST(req: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const session = await getSession()
+4 -3
View File
@@ -4,6 +4,7 @@ import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { hasAdminPermission } from '@/lib/permissions/utils'
import { generateRequestId } from '@/lib/utils'
import { db } from '@/db'
import { templates, workflow } from '@/db/schema'
@@ -13,7 +14,7 @@ export const revalidate = 0
// GET /api/templates/[id] - Retrieve a single template by ID
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const { id } = await params
try {
@@ -77,7 +78,7 @@ const updateTemplateSchema = z.object({
// PUT /api/templates/[id] - Update a template
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const { id } = await params
try {
@@ -163,7 +164,7 @@ export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const { id } = await params
try {
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { db } from '@/db'
import { templateStars, templates } from '@/db/schema'
@@ -13,7 +14,7 @@ export const revalidate = 0
// GET /api/templates/[id]/star - Check if user has starred this template
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const { id } = await params
try {
@@ -47,7 +48,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
// POST /api/templates/[id]/star - Add a star to the template
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const { id } = await params
try {
@@ -123,7 +124,7 @@ export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const { id } = await params
try {
+2 -1
View File
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { db } from '@/db'
import { templates, workflow, workflowBlocks, workflowEdges } from '@/db/schema'
@@ -13,7 +14,7 @@ export const revalidate = 0
// POST /api/templates/[id]/use - Use a template (increment views and create workflow)
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const { id } = await params
try {
+3 -2
View File
@@ -4,6 +4,7 @@ import { v4 as uuidv4 } from 'uuid'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { db } from '@/db'
import { templateStars, templates, workflow } from '@/db/schema'
@@ -82,7 +83,7 @@ const QueryParamsSchema = z.object({
// GET /api/templates - Retrieve templates
export async function GET(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const session = await getSession()
@@ -185,7 +186,7 @@ export async function GET(request: NextRequest) {
// POST /api/templates - Create a new template
export async function POST(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const session = await getSession()
+4 -3
View File
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { getUserId } from '@/app/api/auth/oauth/utils'
import { db } from '@/db'
import { customTools } from '@/db/schema'
@@ -33,7 +34,7 @@ const CustomToolSchema = z.object({
// GET - Fetch all custom tools for the user
export async function GET(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const searchParams = request.nextUrl.searchParams
const workflowId = searchParams.get('workflowId')
@@ -69,7 +70,7 @@ export async function GET(request: NextRequest) {
// POST - Create or update custom tools
export async function POST(req: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const session = await getSession()
@@ -162,7 +163,7 @@ export async function POST(req: NextRequest) {
// DELETE - Delete a custom tool by ID
export async function DELETE(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const searchParams = request.nextUrl.searchParams
const toolId = searchParams.get('id')
+2 -11
View File
@@ -1,8 +1,8 @@
import { type NextRequest, NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('GoogleDriveFileAPI')
@@ -11,11 +11,10 @@ const logger = createLogger('GoogleDriveFileAPI')
* Get a single file from Google Drive
*/
export async function GET(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8) // Generate a short request ID for correlation
const requestId = generateRequestId()
logger.info(`[${requestId}] Google Drive file request received`)
try {
// Get the credential ID and file ID from the query params
const { searchParams } = new URL(request.url)
const credentialId = searchParams.get('credentialId')
const fileId = searchParams.get('fileId')
@@ -31,7 +30,6 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
// Refresh access token if needed using the utility function
const accessToken = await refreshAccessTokenIfNeeded(
credentialId,
authz.credentialOwnerUserId,
@@ -42,7 +40,6 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
}
// Fetch the file from Google Drive API
logger.info(`[${requestId}] Fetching file ${fileId} from Google Drive API`)
const response = await fetch(
`https://www.googleapis.com/drive/v3/files/${fileId}?fields=id,name,mimeType,iconLink,webViewLink,thumbnailLink,createdTime,modifiedTime,size,owners,exportLinks,shortcutDetails&supportsAllDrives=true`,
@@ -69,7 +66,6 @@ export async function GET(request: NextRequest) {
const file = await response.json()
// In case of Google Docs, Sheets, etc., provide the export links
const exportFormats: { [key: string]: string } = {
'application/vnd.google-apps.document': 'application/pdf', // Google Docs to PDF
'application/vnd.google-apps.spreadsheet':
@@ -77,7 +73,6 @@ export async function GET(request: NextRequest) {
'application/vnd.google-apps.presentation': 'application/pdf', // Google Slides to PDF
}
// Resolve shortcuts transparently for UI stability
if (
file.mimeType === 'application/vnd.google-apps.shortcut' &&
file.shortcutDetails?.targetId
@@ -105,20 +100,16 @@ export async function GET(request: NextRequest) {
}
}
// If the file is a Google Docs, Sheets, or Slides file, we need to provide the export link
if (file.mimeType.startsWith('application/vnd.google-apps.')) {
const format = exportFormats[file.mimeType] || 'application/pdf'
if (!file.exportLinks) {
// If export links are not available in the response, try to construct one
file.downloadUrl = `https://www.googleapis.com/drive/v3/files/${file.id}/export?mimeType=${encodeURIComponent(
format
)}`
} else {
// Use the export link from the response if available
file.downloadUrl = file.exportLinks[format]
}
} else {
// For regular files, use the download link
file.downloadUrl = `https://www.googleapis.com/drive/v3/files/${file.id}?alt=media`
}
+2 -9
View File
@@ -2,8 +2,8 @@ import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('GoogleDriveFilesAPI')
@@ -12,20 +12,17 @@ const logger = createLogger('GoogleDriveFilesAPI')
* Get files from Google Drive
*/
export async function GET(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8) // Generate a short request ID for correlation
const requestId = generateRequestId()
logger.info(`[${requestId}] Google Drive files request received`)
try {
// Get the session
const session = await getSession()
// Check if the user is authenticated
if (!session?.user?.id) {
logger.warn(`[${requestId}] Unauthenticated request rejected`)
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
}
// Get the credential ID from the query params
const { searchParams } = new URL(request.url)
const credentialId = searchParams.get('credentialId')
const mimeType = searchParams.get('mimeType')
@@ -38,14 +35,12 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
}
// Authorize use of the credential (supports collaborator credentials via workflow)
const authz = await authorizeCredentialUse(request, { credentialId: credentialId!, workflowId })
if (!authz.ok || !authz.credentialOwnerUserId) {
logger.warn(`[${requestId}] Unauthorized credential access attempt`, authz)
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
// Refresh access token if needed using the utility function
const accessToken = await refreshAccessTokenIfNeeded(
credentialId!,
authz.credentialOwnerUserId,
@@ -56,7 +51,6 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
}
// Build Drive 'q' expression safely
const qParts: string[] = ['trashed = false']
if (folderId) {
qParts.push(`'${folderId.replace(/'/g, "\\'")}' in parents`)
@@ -69,7 +63,6 @@ export async function GET(request: NextRequest) {
}
const q = encodeURIComponent(qParts.join(' and '))
// Fetch files from Google Drive API with shared drives support
const response = await fetch(
`https://www.googleapis.com/drive/v3/files?q=${q}&supportsAllDrives=true&includeItemsFromAllDrives=true&spaces=drive&fields=files(id,name,mimeType,iconLink,webViewLink,thumbnailLink,createdTime,modifiedTime,size,owners,parents)`,
{
+2 -1
View File
@@ -2,6 +2,7 @@ import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { db } from '@/db'
import { account } from '@/db/schema'
@@ -11,7 +12,7 @@ export const dynamic = 'force-dynamic'
const logger = createLogger('GmailLabelAPI')
export async function GET(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
// Get the session
+2 -2
View File
@@ -2,10 +2,10 @@ import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { db } from '@/db'
import { account } from '@/db/schema'
export const dynamic = 'force-dynamic'
const logger = createLogger('GmailLabelsAPI')
@@ -19,7 +19,7 @@ interface GmailLabel {
}
export async function GET(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
// Get the session
@@ -1,8 +1,8 @@
import { type NextRequest, NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('GoogleCalendarAPI')
@@ -21,7 +21,7 @@ interface CalendarListItem {
* Get calendars from Google Calendar
*/
export async function GET(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8) // Generate a short request ID for correlation
const requestId = generateRequestId()
logger.info(`[${requestId}] Google Calendar calendars request received`)
try {
@@ -3,6 +3,7 @@ import { LinearClient } from '@linear/sdk'
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
export const dynamic = 'force-dynamic'
@@ -19,7 +20,7 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Credential and teamId are required' }, { status: 400 })
}
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
+2 -1
View File
@@ -3,6 +3,7 @@ import { LinearClient } from '@linear/sdk'
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
export const dynamic = 'force-dynamic'
@@ -11,7 +12,7 @@ const logger = createLogger('LinearTeamsAPI')
export async function POST(request: Request) {
try {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const body = await request.json()
const { credential, workflowId } = body
@@ -115,7 +115,6 @@ const getChatDisplayName = async (
export async function POST(request: Request) {
try {
const requestId = crypto.randomUUID().slice(0, 8)
const body = await request.json()
const { credential, workflowId } = body
@@ -1,6 +1,7 @@
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
export const dynamic = 'force-dynamic'
@@ -19,7 +20,7 @@ export async function POST(request: Request) {
}
try {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
@@ -2,6 +2,7 @@ import { eq } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { db } from '@/db'
import { account } from '@/db/schema'
@@ -48,7 +49,7 @@ export async function GET(request: Request) {
const accessToken = await refreshAccessTokenIfNeeded(
credentialId,
credentialOwnerUserId,
crypto.randomUUID().slice(0, 8)
generateRequestId()
)
if (!accessToken) {
@@ -1,6 +1,7 @@
import { NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
export const dynamic = 'force-dynamic'
@@ -17,7 +18,7 @@ interface SlackChannel {
export async function POST(request: Request) {
try {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const body = await request.json()
const { credential, workflowId } = body
+2 -1
View File
@@ -1,5 +1,6 @@
import { type NextRequest, NextResponse } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import type { ThinkingToolParams, ThinkingToolResponse } from '@/tools/thinking/types'
const logger = createLogger('ThinkingToolAPI')
@@ -11,7 +12,7 @@ export const dynamic = 'force-dynamic'
* Simply acknowledges the thought by returning it in the output
*/
export async function POST(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const body: ThinkingToolParams = await request.json()
@@ -2,6 +2,7 @@ import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { db } from '@/db'
import { account } from '@/db/schema'
@@ -14,7 +15,7 @@ const logger = createLogger('WealthboxItemAPI')
* Get a single item (note, contact, task) from Wealthbox
*/
export async function GET(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
// Get the session
@@ -2,6 +2,7 @@ import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { db } from '@/db'
import { account } from '@/db/schema'
@@ -24,7 +25,7 @@ interface WealthboxItem {
* Get items (notes, contacts, tasks) from Wealthbox
*/
export async function GET(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const session = await getSession()
@@ -2,6 +2,7 @@ import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { db } from '@/db'
import { apiKey } from '@/db/schema'
@@ -12,7 +13,7 @@ export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const { id } = await params
try {
+3 -2
View File
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { db } from '@/db'
import { user } from '@/db/schema'
@@ -27,7 +28,7 @@ interface UpdateData {
export const dynamic = 'force-dynamic'
export async function PATCH(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const session = await getSession()
@@ -90,7 +91,7 @@ export async function PATCH(request: NextRequest) {
// GET endpoint to fetch current user profile
export async function GET() {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const session = await getSession()
+3 -2
View File
@@ -4,6 +4,7 @@ import { NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { db } from '@/db'
import { settings } from '@/db/schema'
@@ -40,7 +41,7 @@ const defaultSettings = {
}
export async function GET() {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const session = await getSession()
@@ -83,7 +84,7 @@ export async function GET() {
}
export async function PATCH(request: Request) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const session = await getSession()
@@ -9,6 +9,7 @@ import {
verifyUnsubscribeToken,
} from '@/lib/email/unsubscribe'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
const logger = createLogger('UnsubscribeAPI')
@@ -19,7 +20,7 @@ const unsubscribeSchema = z.object({
})
export async function GET(req: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const { searchParams } = new URL(req.url)
@@ -63,7 +64,7 @@ export async function GET(req: NextRequest) {
}
export async function POST(req: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const body = await req.json()
+2 -1
View File
@@ -4,6 +4,7 @@ import OpenAI, { AzureOpenAI } from 'openai'
import { env } from '@/lib/env'
import { getCostMultiplier, isBillingEnabled } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { db } from '@/db'
import { userStats, workflow } from '@/db/schema'
import { getModelPricing } from '@/providers/utils'
@@ -138,7 +139,7 @@ async function updateUserStatsForWand(
}
export async function POST(req: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
logger.info(`[${requestId}] Received wand generation request`)
if (!client) {
+4 -3
View File
@@ -4,6 +4,7 @@ import { getSession } from '@/lib/auth'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { generateRequestId } from '@/lib/utils'
import { getOAuthToken } from '@/app/api/auth/oauth/utils'
import { db } from '@/db'
import { webhook, workflow } from '@/db/schema'
@@ -14,7 +15,7 @@ export const dynamic = 'force-dynamic'
// Get a specific webhook
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const { id } = await params
@@ -83,7 +84,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
// Update a webhook
export async function PATCH(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const { id } = await params
@@ -181,7 +182,7 @@ export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const { id } = await params
+3 -2
View File
@@ -5,6 +5,7 @@ import { getSession } from '@/lib/auth'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { generateRequestId } from '@/lib/utils'
import { getOAuthToken } from '@/app/api/auth/oauth/utils'
import { db } from '@/db'
import { webhook, workflow } from '@/db/schema'
@@ -15,7 +16,7 @@ export const dynamic = 'force-dynamic'
// Get all webhooks for the current user
export async function GET(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const session = await getSession()
@@ -108,7 +109,7 @@ export async function GET(request: NextRequest) {
// Create or Update a webhook
export async function POST(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const userId = (await getSession())?.user?.id
if (!userId) {
+2 -1
View File
@@ -1,6 +1,7 @@
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { db } from '@/db'
import { webhook } from '@/db/schema'
@@ -9,7 +10,7 @@ const logger = createLogger('WebhookTestAPI')
export const dynamic = 'force-dynamic'
export async function GET(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
// Get the webhook ID and provider from the query parameters
@@ -5,6 +5,7 @@ import { checkServerSideUsageLimits } from '@/lib/billing'
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
import { env, isTruthy } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import {
handleSlackChallenge,
handleWhatsAppVerification,
@@ -27,7 +28,7 @@ export const runtime = 'nodejs'
* Handles verification requests from webhook providers and confirms endpoint exists.
*/
export async function GET(request: NextRequest, { params }: { params: Promise<{ path: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const path = (await params).path
@@ -83,7 +84,7 @@ export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ path: string }> }
) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
let foundWorkflow: any = null
let foundWebhook: any = null
@@ -5,6 +5,7 @@ import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { simAgentClient } from '@/lib/sim-agent'
import { generateRequestId } from '@/lib/utils'
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers'
import { getAllBlocks } from '@/blocks/registry'
import type { BlockConfig } from '@/blocks/types'
@@ -48,7 +49,7 @@ type AutoLayoutRequest = z.infer<typeof AutoLayoutRequestSchema>
* Apply autolayout to an existing workflow
*/
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const startTime = Date.now()
const { id: workflowId } = await params
@@ -1,5 +1,6 @@
import { eq } from 'drizzle-orm'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
import { db } from '@/db'
import { chat } from '@/db/schema'
@@ -11,7 +12,7 @@ const logger = createLogger('ChatStatusAPI')
*/
export async function GET(_request: Request, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
logger.debug(`[${requestId}] Checking chat deployment status for workflow: ${id}`)
@@ -12,6 +12,7 @@ describe('Workflow Deployment API Route', () => {
vi.doMock('@/lib/utils', () => ({
generateApiKey: vi.fn().mockReturnValue('sim_testkeygenerated12345'),
generateRequestId: vi.fn(() => 'test-request-id'),
}))
vi.doMock('uuid', () => ({
@@ -2,7 +2,7 @@ import { and, desc, eq } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { createLogger } from '@/lib/logs/console/logger'
import { generateApiKey } from '@/lib/utils'
import { generateApiKey, generateRequestId } from '@/lib/utils'
import { validateWorkflowAccess } from '@/app/api/workflows/middleware'
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
import { db } from '@/db'
@@ -14,7 +14,7 @@ export const dynamic = 'force-dynamic'
export const runtime = 'nodejs'
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const { id } = await params
try {
@@ -133,7 +133,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
}
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const { id } = await params
try {
@@ -368,7 +368,7 @@ export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const { id } = await params
try {
@@ -1,6 +1,7 @@
import { eq } from 'drizzle-orm'
import type { NextRequest, NextResponse } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { validateWorkflowAccess } from '@/app/api/workflows/middleware'
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
import { db } from '@/db'
@@ -18,7 +19,7 @@ function addNoCacheHeaders(response: NextResponse): NextResponse {
}
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const { id } = await params
try {
@@ -1,10 +1,10 @@
import crypto from 'crypto'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { generateRequestId } from '@/lib/utils'
import { db } from '@/db'
import { workflow, workflowBlocks, workflowEdges, workflowSubflows } from '@/db/schema'
import type { Variable } from '@/stores/panel/variables/types'
@@ -23,7 +23,7 @@ const DuplicateRequestSchema = z.object({
// POST /api/workflows/[id]/duplicate - Duplicate a workflow with all its blocks, edges, and subflows
export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id: sourceWorkflowId } = await params
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const startTime = Date.now()
const session = await getSession()
@@ -162,6 +162,7 @@ describe('Workflow Execution API Route', () => {
}),
isHosted: vi.fn().mockReturnValue(false),
getRotatingApiKey: vi.fn().mockReturnValue('rotated-api-key'),
generateRequestId: vi.fn(() => 'test-request-id'),
}))
vi.doMock('@/lib/logs/execution/logging-session', () => ({
@@ -10,7 +10,7 @@ import { getPersonalAndWorkspaceEnv } from '@/lib/environment/utils'
import { createLogger } from '@/lib/logs/console/logger'
import { LoggingSession } from '@/lib/logs/execution/logging-session'
import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
import { decryptSecret } from '@/lib/utils'
import { decryptSecret, generateRequestId } from '@/lib/utils'
import { loadDeployedWorkflowState } from '@/lib/workflows/db-helpers'
import {
createHttpResponseFromBlock,
@@ -342,7 +342,7 @@ async function executeWorkflow(
}
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const { id } = await params
try {
@@ -440,7 +440,7 @@ export async function POST(
request: Request,
{ params }: { params: Promise<{ id: string }> }
): Promise<Response> {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const logger = createLogger('WorkflowExecuteAPI')
logger.info(`[${requestId}] Raw request body: `)
+2 -1
View File
@@ -2,6 +2,7 @@ import type { NextRequest } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { LoggingSession } from '@/lib/logs/execution/logging-session'
import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
import { generateRequestId } from '@/lib/utils'
import { validateWorkflowAccess } from '@/app/api/workflows/middleware'
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
@@ -10,7 +11,7 @@ const logger = createLogger('WorkflowLogAPI')
export const dynamic = 'force-dynamic'
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const { id } = await params
try {
@@ -1,8 +1,8 @@
import crypto from 'crypto'
import { eq } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/db-helpers'
import { validateWorkflowAccess } from '@/app/api/workflows/middleware'
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
@@ -20,7 +20,7 @@ export const runtime = 'nodejs'
* Revert workflow to its deployed state by saving deployed state to normalized tables
*/
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const { id } = await params
try {
+4 -3
View File
@@ -6,6 +6,7 @@ import { verifyInternalToken } from '@/lib/auth/internal'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { getUserEntityPermissions, hasAdminPermission } from '@/lib/permissions/utils'
import { generateRequestId } from '@/lib/utils'
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers'
import { db } from '@/db'
import { apiKey as apiKeyTable, templates, workflow } from '@/db/schema'
@@ -25,7 +26,7 @@ const UpdateWorkflowSchema = z.object({
* Uses hybrid approach: try normalized tables first, fallback to JSON blob
*/
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const startTime = Date.now()
const { id: workflowId } = await params
@@ -169,7 +170,7 @@ export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const startTime = Date.now()
const { id: workflowId } = await params
@@ -306,7 +307,7 @@ export async function DELETE(
* Update workflow metadata (name, description, color, folderId)
*/
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const startTime = Date.now()
const { id: workflowId } = await params
@@ -4,6 +4,7 @@ import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { generateRequestId } from '@/lib/utils'
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/db-helpers'
import { sanitizeAgentToolsInBlocks } from '@/lib/workflows/validation'
import { db } from '@/db'
@@ -112,7 +113,7 @@ const WorkflowStateSchema = z.object({
* Save complete workflow state to normalized database tables
*/
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const startTime = Date.now()
const { id: workflowId } = await params
@@ -1,5 +1,6 @@
import type { NextRequest } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers'
import { hasWorkflowChanged } from '@/lib/workflows/utils'
import { validateWorkflowAccess } from '@/app/api/workflows/middleware'
@@ -8,7 +9,7 @@ import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/
const logger = createLogger('WorkflowStatusAPI')
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const { id } = await params
@@ -4,6 +4,7 @@ import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { generateRequestId } from '@/lib/utils'
import { db } from '@/db'
import { workflow } from '@/db/schema'
import type { Variable } from '@/stores/panel/variables/types'
@@ -23,7 +24,7 @@ const VariablesSchema = z.object({
})
export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const workflowId = (await params).id
try {
@@ -112,7 +113,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
}
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const workflowId = (await params).id
try {
@@ -5,6 +5,7 @@ import { z } from 'zod'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { simAgentClient } from '@/lib/sim-agent'
import { generateRequestId } from '@/lib/utils'
import {
loadWorkflowFromNormalizedTables,
saveWorkflowToNormalizedTables,
@@ -244,7 +245,7 @@ async function upsertCustomToolsFromBlocks(
* Handles copilot edits, imports, and text editor saves
*/
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const startTime = Date.now()
const { id: workflowId } = await params
@@ -1,6 +1,7 @@
import { eq } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
import { db } from '@/db'
import { marketplace, workflow } from '@/db/schema'
@@ -11,7 +12,7 @@ const logger = createLogger('PublicWorkflowAPI')
export const revalidate = 3600
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const { id } = await params
+3 -3
View File
@@ -1,9 +1,9 @@
import crypto from 'crypto'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import { db } from '@/db'
import { workflow, workflowBlocks, workspace } from '@/db/schema'
import { verifyWorkspaceMembership } from './utils'
@@ -20,7 +20,7 @@ const CreateWorkflowSchema = z.object({
// GET /api/workflows - Get workflows for user (optionally filtered by workspaceId)
export async function GET(request: Request) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const startTime = Date.now()
const url = new URL(request.url)
const workspaceId = url.searchParams.get('workspaceId')
@@ -82,7 +82,7 @@ export async function GET(request: Request) {
// POST /api/workflows - Create a new workflow
export async function POST(req: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const session = await getSession()
if (!session?.user?.id) {
@@ -1,6 +1,7 @@
import { type NextRequest, NextResponse } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { simAgentClient } from '@/lib/sim-agent'
import { generateRequestId } from '@/lib/utils'
import { getAllBlocks } from '@/blocks/registry'
import type { BlockConfig } from '@/blocks/types'
import { resolveOutputType } from '@/blocks/utils'
@@ -9,7 +10,7 @@ import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/w
const logger = createLogger('WorkflowYamlAPI')
export async function POST(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
logger.info(`[${requestId}] Converting workflow JSON to YAML`)
@@ -4,6 +4,7 @@ import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { simAgentClient } from '@/lib/sim-agent'
import { generateRequestId } from '@/lib/utils'
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers'
import { getAllBlocks } from '@/blocks/registry'
import type { BlockConfig } from '@/blocks/types'
@@ -15,7 +16,7 @@ import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/w
const logger = createLogger('WorkflowYamlExportAPI')
export async function GET(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const url = new URL(request.url)
const workflowId = url.searchParams.get('workflowId')
@@ -4,7 +4,7 @@ import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { decryptSecret, encryptSecret } from '@/lib/utils'
import { decryptSecret, encryptSecret, generateRequestId } from '@/lib/utils'
import { db } from '@/db'
import { environment, workspace, workspaceEnvironment } from '@/db/schema'
@@ -19,7 +19,7 @@ const DeleteSchema = z.object({
})
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const workspaceId = (await params).id
try {
@@ -102,7 +102,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
}
export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const workspaceId = (await params).id
try {
@@ -169,7 +169,7 @@ export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
const workspaceId = (await params).id
try {
-1
View File
@@ -1,4 +1,3 @@
import crypto from 'crypto'
import { and, desc, eq, isNull } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
+2 -1
View File
@@ -3,6 +3,7 @@ import { z } from 'zod'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/sim-agent'
import { generateRequestId } from '@/lib/utils'
import { getAllBlocks } from '@/blocks/registry'
import type { BlockConfig } from '@/blocks/types'
import { resolveOutputType } from '@/blocks/utils'
@@ -50,7 +51,7 @@ const AutoLayoutRequestSchema = z.object({
})
export async function POST(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const body = await request.json()
+2 -1
View File
@@ -3,6 +3,7 @@ import { z } from 'zod'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/sim-agent'
import { generateRequestId } from '@/lib/utils'
import { getAllBlocks } from '@/blocks/registry'
import type { BlockConfig } from '@/blocks/types'
import { resolveOutputType } from '@/blocks/utils'
@@ -61,7 +62,7 @@ const CreateDiffRequestSchema = z.object({
})
export async function POST(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
logger.info(`[${requestId}] ===== YAML DIFF CREATE API CALLED =====`)
+2 -1
View File
@@ -3,6 +3,7 @@ import { z } from 'zod'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/sim-agent'
import { generateRequestId } from '@/lib/utils'
import { getAllBlocks } from '@/blocks/registry'
import type { BlockConfig } from '@/blocks/types'
import { resolveOutputType } from '@/blocks/utils'
@@ -45,7 +46,7 @@ const MergeDiffRequestSchema = z.object({
})
export async function POST(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const body = await request.json()
+2 -1
View File
@@ -3,6 +3,7 @@ import { z } from 'zod'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/sim-agent'
import { generateRequestId } from '@/lib/utils'
import { getAllBlocks } from '@/blocks/registry'
import type { BlockConfig } from '@/blocks/types'
import { resolveOutputType } from '@/blocks/utils'
@@ -19,7 +20,7 @@ const GenerateRequestSchema = z.object({
})
export async function POST(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const body = await request.json()
+2 -1
View File
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/sim-agent'
import { generateRequestId } from '@/lib/utils'
const logger = createLogger('YamlHealthAPI')
@@ -9,7 +10,7 @@ const logger = createLogger('YamlHealthAPI')
const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT
export async function GET() {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
logger.info(`[${requestId}] Checking YAML service health`)
+2 -1
View File
@@ -3,6 +3,7 @@ import { z } from 'zod'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/sim-agent'
import { generateRequestId } from '@/lib/utils'
import { getAllBlocks } from '@/blocks/registry'
import type { BlockConfig } from '@/blocks/types'
import { resolveOutputType } from '@/blocks/utils'
@@ -18,7 +19,7 @@ const ParseRequestSchema = z.object({
})
export async function POST(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const body = await request.json()
+2 -1
View File
@@ -3,6 +3,7 @@ import { z } from 'zod'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console/logger'
import { SIM_AGENT_API_URL_DEFAULT } from '@/lib/sim-agent'
import { generateRequestId } from '@/lib/utils'
import { getAllBlocks } from '@/blocks/registry'
import type { BlockConfig } from '@/blocks/types'
import { resolveOutputType } from '@/blocks/utils'
@@ -25,7 +26,7 @@ const ConvertRequestSchema = z.object({
})
export async function POST(request: NextRequest) {
const requestId = crypto.randomUUID().slice(0, 8)
const requestId = generateRequestId()
try {
const body = await request.json()

Some files were not shown because too many files have changed in this diff Show More