chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1,60 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { createA2AClient, taskOutput } from '@/lib/a2a/client'
import { a2aCancelTaskContract } from '@/lib/api/contracts/tools/a2a'
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { enforceUserOrIpRateLimit } from '@/lib/core/rate-limiter'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
export const dynamic = 'force-dynamic'
export const maxDuration = 60
const logger = createLogger('A2ACancelTaskAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
if (!auth.success) {
return NextResponse.json(
{ success: false, error: auth.error || 'Authentication required' },
{ status: 401 }
)
}
const rateLimited = await enforceUserOrIpRateLimit('a2a-cancel-task', auth.userId, request)
if (rateLimited) return rateLimited
const parsed = await parseRequest(
a2aCancelTaskContract,
request,
{},
{
validationErrorResponse: (error) =>
NextResponse.json(
{ success: false, error: getValidationErrorMessage(error, 'Invalid request data') },
{ status: 400 }
),
}
)
if (!parsed.success) return parsed.response
const body = parsed.data.body
try {
const client = await createA2AClient(body.agentUrl, body.apiKey, { signal: request.signal })
const task = await client.cancelTask({ tenant: '', id: body.taskId, metadata: undefined })
const out = taskOutput(task)
logger.info(`[${requestId}] Cancel requested for A2A task ${task.id}`)
return NextResponse.json({
success: true,
output: { taskId: out.taskId, state: out.state, canceled: out.state === 'canceled' },
})
} catch (error) {
logger.error(`[${requestId}] A2A cancel-task failed`, { error: getErrorMessage(error) })
return NextResponse.json({ success: false, error: getErrorMessage(error) }, { status: 502 })
}
})
@@ -0,0 +1,56 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { agentCardOutput, createA2AClient } from '@/lib/a2a/client'
import { a2aGetAgentCardContract } from '@/lib/api/contracts/tools/a2a'
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { enforceUserOrIpRateLimit } from '@/lib/core/rate-limiter'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
export const dynamic = 'force-dynamic'
export const maxDuration = 60
const logger = createLogger('A2AGetAgentCardAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
if (!auth.success) {
return NextResponse.json(
{ success: false, error: auth.error || 'Authentication required' },
{ status: 401 }
)
}
const rateLimited = await enforceUserOrIpRateLimit('a2a-get-agent-card', auth.userId, request)
if (rateLimited) return rateLimited
const parsed = await parseRequest(
a2aGetAgentCardContract,
request,
{},
{
validationErrorResponse: (error) =>
NextResponse.json(
{ success: false, error: getValidationErrorMessage(error, 'Invalid request data') },
{ status: 400 }
),
}
)
if (!parsed.success) return parsed.response
const body = parsed.data.body
try {
const client = await createA2AClient(body.agentUrl, body.apiKey, { signal: request.signal })
const card = await client.getAgentCard()
logger.info(`[${requestId}] Fetched agent card for ${card.name}`)
return NextResponse.json({ success: true, output: agentCardOutput(card, body.agentUrl) })
} catch (error) {
logger.error(`[${requestId}] A2A get-agent-card failed`, { error: getErrorMessage(error) })
return NextResponse.json({ success: false, error: getErrorMessage(error) }, { status: 502 })
}
})
@@ -0,0 +1,60 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { createA2AClient, taskOutput } from '@/lib/a2a/client'
import { a2aGetTaskContract } from '@/lib/api/contracts/tools/a2a'
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { enforceUserOrIpRateLimit } from '@/lib/core/rate-limiter'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
export const dynamic = 'force-dynamic'
export const maxDuration = 60
const logger = createLogger('A2AGetTaskAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
if (!auth.success) {
return NextResponse.json(
{ success: false, error: auth.error || 'Authentication required' },
{ status: 401 }
)
}
const rateLimited = await enforceUserOrIpRateLimit('a2a-get-task', auth.userId, request)
if (rateLimited) return rateLimited
const parsed = await parseRequest(
a2aGetTaskContract,
request,
{},
{
validationErrorResponse: (error) =>
NextResponse.json(
{ success: false, error: getValidationErrorMessage(error, 'Invalid request data') },
{ status: 400 }
),
}
)
if (!parsed.success) return parsed.response
const body = parsed.data.body
try {
const client = await createA2AClient(body.agentUrl, body.apiKey, { signal: request.signal })
const task = await client.getTask({
tenant: '',
id: body.taskId,
historyLength: body.historyLength,
})
logger.info(`[${requestId}] Retrieved A2A task ${task.id}`)
return NextResponse.json({ success: true, output: taskOutput(task) })
} catch (error) {
logger.error(`[${requestId}] A2A get-task failed`, { error: getErrorMessage(error) })
return NextResponse.json({ success: false, error: getErrorMessage(error) }, { status: 502 })
}
})
@@ -0,0 +1,145 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import {
type A2AFileInput,
buildUserMessage,
createA2AClient,
isTaskResult,
messageOutput,
taskErrored,
taskOutput,
} from '@/lib/a2a/client'
import { a2aSendMessageContract } from '@/lib/api/contracts/tools/a2a'
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { enforceUserOrIpRateLimit } from '@/lib/core/rate-limiter'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadServableFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { docNotReadyResponse } from '@/lib/uploads/utils/servable-file-response'
import { assertToolFileAccess } from '@/app/api/files/authorization'
export const dynamic = 'force-dynamic'
/** Blocking sends wait until the agent reaches a terminal/interrupted state. */
export const maxDuration = 300
/** Per-file cap on attachments resolved from storage. */
const A2A_MAX_FILE_BYTES = 10 * 1024 * 1024
const logger = createLogger('A2ASendMessageAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
if (!auth.success) {
return NextResponse.json(
{ success: false, error: auth.error || 'Authentication required' },
{ status: 401 }
)
}
const rateLimited = await enforceUserOrIpRateLimit('a2a-send-message', auth.userId, request)
if (rateLimited) return rateLimited
const parsed = await parseRequest(
a2aSendMessageContract,
request,
{},
{
validationErrorResponse: (error) =>
NextResponse.json(
{ success: false, error: getValidationErrorMessage(error, 'Invalid request data') },
{ status: 400 }
),
}
)
if (!parsed.success) return parsed.response
const body = parsed.data.body
let data: unknown
if (body.data !== undefined) {
if (typeof body.data === 'string') {
try {
data = JSON.parse(body.data)
} catch {
return NextResponse.json(
{ success: false, error: 'Data must be valid JSON' },
{ status: 400 }
)
}
} else {
data = body.data
}
}
try {
let files: A2AFileInput[] | undefined
if (body.files?.length) {
if (!auth.userId) {
return NextResponse.json(
{ success: false, error: 'Authentication required to attach files' },
{ status: 401 }
)
}
const userFiles = processFilesToUserFiles(body.files, requestId, logger)
for (const userFile of userFiles) {
const denied = await assertToolFileAccess(userFile.key, auth.userId, requestId, logger)
if (denied) return denied
}
files = await Promise.all(
userFiles.map(async (userFile) => {
const { buffer, contentType } = await downloadServableFileFromStorage(
userFile,
requestId,
logger,
{ maxBytes: A2A_MAX_FILE_BYTES }
)
return {
bytes: buffer,
name: userFile.name,
mediaType: contentType || userFile.type || 'application/octet-stream',
}
})
)
}
const client = await createA2AClient(body.agentUrl, body.apiKey, { signal: request.signal })
const message = buildUserMessage({
text: body.message,
data,
files,
taskId: body.taskId,
contextId: body.contextId,
})
const result = await client.sendMessage({
tenant: '',
message,
configuration: undefined,
metadata: undefined,
})
if (!isTaskResult(result)) {
logger.info(`[${requestId}] A2A send returned a direct message`)
return NextResponse.json({ success: true, output: messageOutput(result) })
}
const output = taskOutput(result)
const errored = taskErrored(result)
logger.info(`[${requestId}] A2A send produced task ${result.id} (${output.state})`)
return NextResponse.json({
success: !errored,
...(errored ? { error: output.content || `Agent task ${output.state}` } : {}),
output,
})
} catch (error) {
const notReady = docNotReadyResponse(error)
if (notReady) return notReady
logger.error(`[${requestId}] A2A send failed`, { error: getErrorMessage(error) })
return NextResponse.json({ success: false, error: getErrorMessage(error) }, { status: 502 })
}
})
@@ -0,0 +1,147 @@
/**
* @vitest-environment node
*/
import {
createMockRequest,
hybridAuthMockFns,
inputValidationMock,
inputValidationMockFns,
} from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { mockProcessFilesToUserFiles, mockDownloadFileFromStorage, mockAssertToolFileAccess } =
vi.hoisted(() => ({
mockProcessFilesToUserFiles: vi.fn(),
mockDownloadFileFromStorage: vi.fn(),
mockAssertToolFileAccess: vi.fn(),
}))
vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock)
vi.mock('@/lib/uploads/utils/file-utils', () => ({
processFilesToUserFiles: mockProcessFilesToUserFiles,
}))
vi.mock('@/lib/uploads/utils/file-utils.server', () => ({
downloadServableFileFromStorage: mockDownloadFileFromStorage,
}))
vi.mock('@/app/api/files/authorization', () => ({
assertToolFileAccess: mockAssertToolFileAccess,
}))
import { POST } from '@/app/api/tools/agiloft/attach/route'
const PINNED_IP = '93.184.216.34'
const baseBody = {
instanceUrl: 'https://example.agiloft.com',
knowledgeBase: 'demo',
login: 'admin',
password: 'secret',
table: 'contracts',
recordId: '42',
fieldName: 'attachments',
file: { key: 's3://bucket/file.txt', name: 'file.txt', size: 5, type: 'text/plain' },
fileName: 'file.txt',
}
function mockSecureFetchResponse(body: {
ok?: boolean
status?: number
json?: unknown
text?: string
}) {
return {
ok: body.ok ?? true,
status: body.status ?? 200,
statusText: '',
headers: new Headers(),
body: null,
text: async () => body.text ?? '',
json: async () => body.json ?? {},
arrayBuffer: async () => new ArrayBuffer(0),
}
}
beforeEach(() => {
vi.clearAllMocks()
hybridAuthMockFns.mockCheckInternalAuth.mockResolvedValue({
success: true,
userId: 'user-1',
authType: 'internal_jwt',
})
inputValidationMockFns.mockValidateUrlWithDNS.mockResolvedValue({
isValid: true,
resolvedIP: PINNED_IP,
originalHostname: 'example.agiloft.com',
})
mockProcessFilesToUserFiles.mockReturnValue([
{ key: 's3://bucket/file.txt', name: 'file.txt', size: 5, type: 'text/plain' },
])
mockAssertToolFileAccess.mockResolvedValue(null)
mockDownloadFileFromStorage.mockResolvedValue({
buffer: Buffer.from('hello'),
contentType: 'application/octet-stream',
})
})
describe('POST /api/tools/agiloft/attach', () => {
it('rejects unauthenticated requests', async () => {
hybridAuthMockFns.mockCheckInternalAuth.mockResolvedValueOnce({
success: false,
error: 'unauthorized',
})
const response = await POST(createMockRequest('POST', baseBody))
expect(response.status).toBe(401)
expect(inputValidationMockFns.mockSecureFetchWithPinnedIP).not.toHaveBeenCalled()
})
it('blocks SSRF when the instance URL fails DNS validation', async () => {
inputValidationMockFns.mockValidateUrlWithDNS.mockResolvedValueOnce({
isValid: false,
error: 'instanceUrl resolves to a blocked IP address',
})
const response = await POST(
createMockRequest('POST', { ...baseBody, instanceUrl: 'https://attacker.example.com' })
)
expect(response.status).toBe(400)
expect(inputValidationMockFns.mockSecureFetchWithPinnedIP).not.toHaveBeenCalled()
})
it('pins the resolved IP for login, attach, and logout (TOCTOU fix)', async () => {
inputValidationMockFns.mockSecureFetchWithPinnedIP
.mockResolvedValueOnce(mockSecureFetchResponse({ json: { access_token: 'tok-att' } }))
.mockResolvedValueOnce(mockSecureFetchResponse({ text: '1' }))
.mockResolvedValueOnce(mockSecureFetchResponse({}))
const response = await POST(createMockRequest('POST', baseBody))
expect(response.status).toBe(200)
const data = (await response.json()) as {
success: true
output: { totalAttachments: number; fileName: string }
}
expect(data.output.totalAttachments).toBe(1)
expect(data.output.fileName).toBe('file.txt')
const calls = inputValidationMockFns.mockSecureFetchWithPinnedIP.mock.calls
expect(calls).toHaveLength(3)
for (const call of calls) {
expect(call[1]).toBe(PINNED_IP)
}
expect(calls[0][0]).toContain('https://example.agiloft.com/ewws/EWLogin')
expect(calls[1][0]).toContain('https://example.agiloft.com/ewws/EWAttach')
expect(calls[1][2]).toMatchObject({
method: 'PUT',
headers: {
Authorization: 'Bearer tok-att',
'Content-Type': 'application/octet-stream',
},
})
expect(calls[2][0]).toContain('https://example.agiloft.com/ewws/EWLogout')
// DNS only resolved once.
expect(inputValidationMockFns.mockValidateUrlWithDNS).toHaveBeenCalledTimes(1)
})
})
@@ -0,0 +1,161 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { agiloftAttachContract } from '@/lib/api/contracts/tools/agiloft'
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { secureFetchWithPinnedIP } from '@/lib/core/security/input-validation.server'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import type { RawFileInput } from '@/lib/uploads/utils/file-schemas'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadServableFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { docNotReadyResponse } from '@/lib/uploads/utils/servable-file-response'
import { assertToolFileAccess } from '@/app/api/files/authorization'
import { buildAttachFileUrl } from '@/tools/agiloft/utils'
import {
agiloftLoginPinned,
agiloftLogoutPinned,
resolveAgiloftInstance,
} from '@/tools/agiloft/utils.server'
export const dynamic = 'force-dynamic'
const logger = createLogger('AgiloftAttachAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Agiloft attach attempt: ${authResult.error}`)
return NextResponse.json(
{ success: false, error: authResult.error || 'Authentication required' },
{ status: 401 }
)
}
const parsed = await parseRequest(
agiloftAttachContract,
request,
{},
{
validationErrorResponse: (error) => {
logger.warn(`[${requestId}] Invalid request data`, { errors: error.issues })
return NextResponse.json(
{
success: false,
error: getValidationErrorMessage(error, 'Invalid request data'),
details: error.issues,
},
{ status: 400 }
)
},
}
)
if (!parsed.success) return parsed.response
const data = parsed.data.body
if (!data.file) {
return NextResponse.json({ success: false, error: 'File is required' }, { status: 400 })
}
const userFiles = processFilesToUserFiles([data.file as RawFileInput], requestId, logger)
if (userFiles.length === 0) {
return NextResponse.json({ success: false, error: 'Invalid file input' }, { status: 400 })
}
const userFile = userFiles[0]
logger.info(
`[${requestId}] Downloading file for Agiloft attach: ${userFile.name} (${userFile.size} bytes)`
)
const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger)
if (denied) return denied
let fileBuffer: Buffer
try {
const servable = await downloadServableFileFromStorage(userFile, requestId, logger)
fileBuffer = servable.buffer
} catch (error) {
const notReady = docNotReadyResponse(error)
if (notReady) return notReady
logger.error(`[${requestId}] Failed to download file from storage:`, error)
return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 })
}
const resolvedFileName = data.fileName || userFile.name || 'attachment'
let resolvedIP: string
try {
resolvedIP = await resolveAgiloftInstance(data.instanceUrl)
} catch (error) {
logger.warn(`[${requestId}] SSRF attempt blocked for Agiloft instance URL`, {
instanceUrl: data.instanceUrl,
})
return NextResponse.json({ success: false, error: toError(error).message }, { status: 400 })
}
const token = await agiloftLoginPinned(data, resolvedIP)
const base = data.instanceUrl.replace(/\/$/, '')
try {
const url = buildAttachFileUrl(base, data, resolvedFileName)
logger.info(`[${requestId}] Uploading file to Agiloft: ${resolvedFileName}`)
const agiloftResponse = await secureFetchWithPinnedIP(url, resolvedIP, {
method: 'PUT',
headers: {
'Content-Type': 'application/octet-stream',
Authorization: `Bearer ${token}`,
},
body: new Uint8Array(fileBuffer),
})
if (!agiloftResponse.ok) {
const errorText = await agiloftResponse.text()
logger.error(
`[${requestId}] Agiloft attach error: ${agiloftResponse.status} - ${errorText}`
)
return NextResponse.json(
{ success: false, error: `Agiloft error: ${agiloftResponse.status} - ${errorText}` },
{ status: agiloftResponse.status }
)
}
let totalAttachments = 0
const responseText = await agiloftResponse.text()
try {
const responseData = JSON.parse(responseText)
const result = responseData.result ?? responseData
totalAttachments = typeof result === 'number' ? result : (result.count ?? result.total ?? 1)
} catch {
totalAttachments = Number(responseText) || 1
}
logger.info(
`[${requestId}] File attached successfully. Total attachments: ${totalAttachments}`
)
return NextResponse.json({
success: true,
output: {
recordId: data.recordId.trim(),
fieldName: data.fieldName.trim(),
fileName: resolvedFileName,
totalAttachments,
},
})
} finally {
await agiloftLogoutPinned(data.instanceUrl, data.knowledgeBase, token, resolvedIP)
}
} catch (error) {
logger.error(`[${requestId}] Error attaching file to Agiloft:`, error)
return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 })
}
})
@@ -0,0 +1,106 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { agiloftAttachmentInfoContract } from '@/lib/api/contracts/tools/agiloft'
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import type { AgiloftAttachmentInfoResponse } from '@/tools/agiloft/types'
import { buildAttachmentInfoUrl } from '@/tools/agiloft/utils'
import { executeAgiloftRequest } from '@/tools/agiloft/utils.server'
export const dynamic = 'force-dynamic'
const logger = createLogger('AgiloftAttachmentInfoAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success || !authResult.userId) {
logger.warn(
`[${requestId}] Unauthorized Agiloft attachment_info attempt: ${authResult.error}`
)
return NextResponse.json(
{ success: false, error: authResult.error || 'Authentication required' },
{ status: 401 }
)
}
const parsed = await parseRequest(
agiloftAttachmentInfoContract,
request,
{},
{
validationErrorResponse: (error) => {
logger.warn(`[${requestId}] Invalid request data`, { errors: error.issues })
return NextResponse.json(
{
success: false,
error: getValidationErrorMessage(error, 'Invalid request data'),
details: error.issues,
},
{ status: 400 }
)
},
}
)
if (!parsed.success) return parsed.response
const params = parsed.data.body
const result = await executeAgiloftRequest<AgiloftAttachmentInfoResponse>(
params,
(base) => ({
url: buildAttachmentInfoUrl(base, params),
method: 'GET',
}),
async (response) => {
if (!response.ok) {
const errorText = await response.text()
return {
success: false,
output: { attachments: [], totalCount: 0 },
error: `Agiloft error: ${response.status} - ${errorText}`,
}
}
const data = (await response.json()) as Record<string, unknown>
const result = (data.result ?? data) as Record<string, unknown>
const attachments: Array<{ position: number; name: string; size: number }> = []
if (Array.isArray(result)) {
for (let i = 0; i < result.length; i++) {
const item = result[i] as Record<string, unknown>
attachments.push({
position: (item.filePosition as number) ?? (item.position as number) ?? i,
name:
(item.fileName as string) ??
(item.name as string) ??
(item.filename as string) ??
'',
size: (item.size as number) ?? (item.fileSize as number) ?? 0,
})
}
}
return {
success: data.success !== false,
output: {
attachments,
totalCount: attachments.length,
},
}
}
)
return NextResponse.json(result)
} catch (error) {
logger.error(`[${requestId}] Error getting Agiloft attachment info:`, error)
return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 })
}
})
@@ -0,0 +1,101 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { agiloftCreateRecordContract } from '@/lib/api/contracts/tools/agiloft'
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import type { AgiloftRecordResponse } from '@/tools/agiloft/types'
import { buildCreateRecordUrl } from '@/tools/agiloft/utils'
import { executeAgiloftRequest } from '@/tools/agiloft/utils.server'
export const dynamic = 'force-dynamic'
const logger = createLogger('AgiloftCreateRecordAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Agiloft create_record attempt: ${authResult.error}`)
return NextResponse.json(
{ success: false, error: authResult.error || 'Authentication required' },
{ status: 401 }
)
}
const parsed = await parseRequest(
agiloftCreateRecordContract,
request,
{},
{
validationErrorResponse: (error) => {
logger.warn(`[${requestId}] Invalid request data`, { errors: error.issues })
return NextResponse.json(
{
success: false,
error: getValidationErrorMessage(error, 'Invalid request data'),
details: error.issues,
},
{ status: 400 }
)
},
}
)
if (!parsed.success) return parsed.response
const params = parsed.data.body
let body: string
try {
body = JSON.stringify(JSON.parse(params.data))
} catch {
return NextResponse.json({
success: false,
output: { id: null, fields: {} },
error: 'Invalid JSON in data parameter',
})
}
const result = await executeAgiloftRequest<AgiloftRecordResponse>(
params,
(base) => ({
url: buildCreateRecordUrl(base, params),
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body,
}),
async (response) => {
if (!response.ok) {
const errorText = await response.text()
return {
success: false,
output: { id: null, fields: {} },
error: `Agiloft error: ${response.status} - ${errorText}`,
}
}
const data = (await response.json()) as Record<string, unknown>
const result = (data.result ?? data) as Record<string, unknown>
const id = result.id ?? result.ID ?? data.id ?? data.ID ?? null
return {
success: data.success !== false,
output: {
id: id != null ? String(id) : null,
fields: result ?? {},
},
}
}
)
return NextResponse.json(result)
} catch (error) {
logger.error(`[${requestId}] Error creating Agiloft record:`, error)
return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 })
}
})
@@ -0,0 +1,85 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { agiloftDeleteRecordContract } from '@/lib/api/contracts/tools/agiloft'
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import type { AgiloftDeleteResponse } from '@/tools/agiloft/types'
import { buildDeleteRecordUrl } from '@/tools/agiloft/utils'
import { executeAgiloftRequest } from '@/tools/agiloft/utils.server'
export const dynamic = 'force-dynamic'
const logger = createLogger('AgiloftDeleteRecordAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Agiloft delete_record attempt: ${authResult.error}`)
return NextResponse.json(
{ success: false, error: authResult.error || 'Authentication required' },
{ status: 401 }
)
}
const parsed = await parseRequest(
agiloftDeleteRecordContract,
request,
{},
{
validationErrorResponse: (error) => {
logger.warn(`[${requestId}] Invalid request data`, { errors: error.issues })
return NextResponse.json(
{
success: false,
error: getValidationErrorMessage(error, 'Invalid request data'),
details: error.issues,
},
{ status: 400 }
)
},
}
)
if (!parsed.success) return parsed.response
const params = parsed.data.body
const result = await executeAgiloftRequest<AgiloftDeleteResponse>(
params,
(base) => ({
url: buildDeleteRecordUrl(base, params),
method: 'DELETE',
headers: { Accept: 'application/json' },
}),
async (response) => {
if (!response.ok) {
const errorText = await response.text()
return {
success: false,
output: { id: params.recordId?.trim() ?? '', deleted: false },
error: `Agiloft error: ${response.status} - ${errorText}`,
}
}
return {
success: true,
output: {
id: params.recordId?.trim() ?? '',
deleted: true,
},
}
}
)
return NextResponse.json(result)
} catch (error) {
logger.error(`[${requestId}] Error deleting Agiloft record:`, error)
return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 })
}
})
@@ -0,0 +1,112 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { agiloftGetChoiceLineIdContract } from '@/lib/api/contracts/tools/agiloft'
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import type { AgiloftGetChoiceLineIdResponse } from '@/tools/agiloft/types'
import { buildGetChoiceLineIdUrl } from '@/tools/agiloft/utils'
import { executeAgiloftRequest } from '@/tools/agiloft/utils.server'
export const dynamic = 'force-dynamic'
const logger = createLogger('AgiloftGetChoiceLineIdAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success || !authResult.userId) {
logger.warn(
`[${requestId}] Unauthorized Agiloft get_choice_line_id attempt: ${authResult.error}`
)
return NextResponse.json(
{ success: false, error: authResult.error || 'Authentication required' },
{ status: 401 }
)
}
const parsed = await parseRequest(
agiloftGetChoiceLineIdContract,
request,
{},
{
validationErrorResponse: (error) => {
logger.warn(`[${requestId}] Invalid request data`, { errors: error.issues })
return NextResponse.json(
{
success: false,
error: getValidationErrorMessage(error, 'Invalid request data'),
details: error.issues,
},
{ status: 400 }
)
},
}
)
if (!parsed.success) return parsed.response
const params = parsed.data.body
const result = await executeAgiloftRequest<AgiloftGetChoiceLineIdResponse>(
params,
(base) => ({
url: buildGetChoiceLineIdUrl(base, params),
method: 'GET',
headers: { Accept: 'application/json' },
}),
async (response) => {
if (!response.ok) {
const errorText = await response.text()
return {
success: false,
output: { choiceLineId: null },
error: `Agiloft error: ${response.status} - ${errorText}`,
}
}
const data = (await response.json()) as Record<string, unknown>
const result = data.result ?? data
let choiceLineId: number | null = null
if (typeof result === 'number') {
choiceLineId = result
} else if (typeof result === 'string') {
const parsed = Number(result)
choiceLineId = Number.isFinite(parsed) ? parsed : null
} else if (typeof result === 'object' && result !== null) {
const obj = result as Record<string, unknown>
const idVal = obj.id ?? obj.choiceLineId ?? obj.lineId
if (typeof idVal === 'number') {
choiceLineId = idVal
} else if (typeof idVal === 'string') {
const parsed = Number(idVal)
choiceLineId = Number.isFinite(parsed) ? parsed : null
}
}
if (choiceLineId === null) {
return {
success: false,
output: { choiceLineId: null },
error: `No choice line ID found for value "${params.value}" in field "${params.fieldName}"`,
}
}
return {
success: data.success !== false,
output: { choiceLineId },
}
}
)
return NextResponse.json(result)
} catch (error) {
logger.error(`[${requestId}] Error getting Agiloft choice line ID:`, error)
return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 })
}
})
@@ -0,0 +1,99 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { agiloftLockRecordContract } from '@/lib/api/contracts/tools/agiloft'
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import type { AgiloftLockResponse } from '@/tools/agiloft/types'
import { buildLockRecordUrl, getLockHttpMethod } from '@/tools/agiloft/utils'
import { executeAgiloftRequest } from '@/tools/agiloft/utils.server'
export const dynamic = 'force-dynamic'
const logger = createLogger('AgiloftLockRecordAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Agiloft lock_record attempt: ${authResult.error}`)
return NextResponse.json(
{ success: false, error: authResult.error || 'Authentication required' },
{ status: 401 }
)
}
const parsed = await parseRequest(
agiloftLockRecordContract,
request,
{},
{
validationErrorResponse: (error) => {
logger.warn(`[${requestId}] Invalid request data`, { errors: error.issues })
return NextResponse.json(
{
success: false,
error: getValidationErrorMessage(error, 'Invalid request data'),
details: error.issues,
},
{ status: 400 }
)
},
}
)
if (!parsed.success) return parsed.response
const params = parsed.data.body
const result = await executeAgiloftRequest<AgiloftLockResponse>(
params,
(base) => ({
url: buildLockRecordUrl(base, params),
method: getLockHttpMethod(params.lockAction),
}),
async (response) => {
if (!response.ok) {
const errorText = await response.text()
return {
success: false,
output: {
id: params.recordId?.trim() ?? '',
lockStatus: 'UNKNOWN',
lockedBy: null,
lockExpiresInMinutes: null,
},
error: `Agiloft error: ${response.status} - ${errorText}`,
}
}
const data = (await response.json()) as Record<string, unknown>
const result = (data.result ?? data) as Record<string, unknown>
return {
success: data.success !== false,
output: {
id: String(result.id ?? params.recordId?.trim() ?? ''),
lockStatus:
(result.lock_status as string) ?? (result.lockStatus as string) ?? 'UNKNOWN',
lockedBy:
(result.locked_by as string | null) ?? (result.lockedBy as string | null) ?? null,
lockExpiresInMinutes:
(result.lock_expires_in_minutes as number | null) ??
(result.lockExpiresInMinutes as number | null) ??
null,
},
}
}
)
return NextResponse.json(result)
} catch (error) {
logger.error(`[${requestId}] Error locking Agiloft record:`, error)
return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 })
}
})
@@ -0,0 +1,89 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { agiloftReadRecordContract } from '@/lib/api/contracts/tools/agiloft'
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import type { AgiloftRecordResponse } from '@/tools/agiloft/types'
import { buildReadRecordUrl } from '@/tools/agiloft/utils'
import { executeAgiloftRequest } from '@/tools/agiloft/utils.server'
export const dynamic = 'force-dynamic'
const logger = createLogger('AgiloftReadRecordAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Agiloft read_record attempt: ${authResult.error}`)
return NextResponse.json(
{ success: false, error: authResult.error || 'Authentication required' },
{ status: 401 }
)
}
const parsed = await parseRequest(
agiloftReadRecordContract,
request,
{},
{
validationErrorResponse: (error) => {
logger.warn(`[${requestId}] Invalid request data`, { errors: error.issues })
return NextResponse.json(
{
success: false,
error: getValidationErrorMessage(error, 'Invalid request data'),
details: error.issues,
},
{ status: 400 }
)
},
}
)
if (!parsed.success) return parsed.response
const params = parsed.data.body
const result = await executeAgiloftRequest<AgiloftRecordResponse>(
params,
(base) => ({
url: buildReadRecordUrl(base, params),
method: 'GET',
headers: { Accept: 'application/json' },
}),
async (response) => {
if (!response.ok) {
const errorText = await response.text()
return {
success: false,
output: { id: null, fields: {} },
error: `Agiloft error: ${response.status} - ${errorText}`,
}
}
const data = (await response.json()) as Record<string, unknown>
const result = (data.result ?? data) as Record<string, unknown>
const id = result.id ?? result.ID ?? data.id ?? data.ID ?? null
return {
success: data.success !== false,
output: {
id: id != null ? String(id) : null,
fields: result ?? {},
},
}
}
)
return NextResponse.json(result)
} catch (error) {
logger.error(`[${requestId}] Error reading Agiloft record:`, error)
return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 })
}
})
@@ -0,0 +1,102 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { agiloftRemoveAttachmentContract } from '@/lib/api/contracts/tools/agiloft'
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import type { AgiloftRemoveAttachmentResponse } from '@/tools/agiloft/types'
import { buildRemoveAttachmentUrl } from '@/tools/agiloft/utils'
import { executeAgiloftRequest } from '@/tools/agiloft/utils.server'
export const dynamic = 'force-dynamic'
const logger = createLogger('AgiloftRemoveAttachmentAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success || !authResult.userId) {
logger.warn(
`[${requestId}] Unauthorized Agiloft remove_attachment attempt: ${authResult.error}`
)
return NextResponse.json(
{ success: false, error: authResult.error || 'Authentication required' },
{ status: 401 }
)
}
const parsed = await parseRequest(
agiloftRemoveAttachmentContract,
request,
{},
{
validationErrorResponse: (error) => {
logger.warn(`[${requestId}] Invalid request data`, { errors: error.issues })
return NextResponse.json(
{
success: false,
error: getValidationErrorMessage(error, 'Invalid request data'),
details: error.issues,
},
{ status: 400 }
)
},
}
)
if (!parsed.success) return parsed.response
const params = parsed.data.body
const result = await executeAgiloftRequest<AgiloftRemoveAttachmentResponse>(
params,
(base) => ({
url: buildRemoveAttachmentUrl(base, params),
method: 'DELETE',
}),
async (response) => {
const text = await response.text()
if (!response.ok) {
return {
success: false,
output: {
recordId: params.recordId?.trim() ?? '',
fieldName: params.fieldName?.trim() ?? '',
remainingAttachments: 0,
},
error: `Agiloft error: ${response.status} - ${text}`,
}
}
let remainingAttachments = 0
try {
const data = JSON.parse(text)
const result = data.result ?? data
remainingAttachments =
typeof result === 'number' ? result : (result.count ?? result.remaining ?? 0)
} catch {
remainingAttachments = Number(text) || 0
}
return {
success: true,
output: {
recordId: params.recordId?.trim() ?? '',
fieldName: params.fieldName?.trim() ?? '',
remainingAttachments,
},
}
}
)
return NextResponse.json(result)
} catch (error) {
logger.error(`[${requestId}] Error removing Agiloft attachment:`, error)
return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 })
}
})
@@ -0,0 +1,163 @@
/**
* @vitest-environment node
*/
import {
createMockRequest,
hybridAuthMockFns,
inputValidationMock,
inputValidationMockFns,
} from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock)
import { POST } from '@/app/api/tools/agiloft/retrieve/route'
const PINNED_IP = '93.184.216.34'
const baseBody = {
instanceUrl: 'https://example.agiloft.com',
knowledgeBase: 'demo',
login: 'admin',
password: 'secret',
table: 'contracts',
recordId: '42',
fieldName: 'attachments',
position: '0',
}
function mockSecureFetchResponse(body: {
ok?: boolean
status?: number
json?: unknown
text?: string
arrayBuffer?: ArrayBuffer
headers?: Headers
}) {
return {
ok: body.ok ?? true,
status: body.status ?? 200,
statusText: '',
headers: body.headers ?? new Headers(),
body: null,
text: async () => body.text ?? '',
json: async () => body.json ?? {},
arrayBuffer: async () => body.arrayBuffer ?? new ArrayBuffer(0),
}
}
beforeEach(() => {
vi.clearAllMocks()
hybridAuthMockFns.mockCheckInternalAuth.mockResolvedValue({
success: true,
userId: 'user-1',
authType: 'internal_jwt',
})
inputValidationMockFns.mockValidateUrlWithDNS.mockResolvedValue({
isValid: true,
resolvedIP: PINNED_IP,
originalHostname: 'example.agiloft.com',
})
})
describe('POST /api/tools/agiloft/retrieve', () => {
it('rejects unauthenticated requests', async () => {
hybridAuthMockFns.mockCheckInternalAuth.mockResolvedValueOnce({
success: false,
error: 'unauthorized',
})
const response = await POST(createMockRequest('POST', baseBody))
expect(response.status).toBe(401)
expect(inputValidationMockFns.mockSecureFetchWithPinnedIP).not.toHaveBeenCalled()
})
it('blocks SSRF when the instance URL fails DNS validation', async () => {
inputValidationMockFns.mockValidateUrlWithDNS.mockResolvedValueOnce({
isValid: false,
error: 'instanceUrl resolves to a blocked IP address',
})
const response = await POST(
createMockRequest('POST', { ...baseBody, instanceUrl: 'https://attacker.example.com' })
)
expect(response.status).toBe(400)
const data = (await response.json()) as { success: false; error: string }
expect(data.success).toBe(false)
expect(data.error).toContain('blocked IP')
expect(inputValidationMockFns.mockSecureFetchWithPinnedIP).not.toHaveBeenCalled()
})
it('pins the resolved IP for login, retrieve, and logout (TOCTOU fix)', async () => {
const fileBytes = Buffer.from('hello-attachment', 'utf-8')
inputValidationMockFns.mockSecureFetchWithPinnedIP
.mockResolvedValueOnce(mockSecureFetchResponse({ json: { access_token: 'tok-xyz' } }))
.mockResolvedValueOnce(
mockSecureFetchResponse({
arrayBuffer: fileBytes.buffer.slice(
fileBytes.byteOffset,
fileBytes.byteOffset + fileBytes.byteLength
) as ArrayBuffer,
headers: new Headers({
'content-type': 'text/plain',
'content-disposition': 'attachment; filename="report.txt"',
}),
})
)
.mockResolvedValueOnce(mockSecureFetchResponse({}))
const response = await POST(createMockRequest('POST', baseBody))
expect(response.status).toBe(200)
const data = (await response.json()) as {
success: true
output: { file: { name: string; mimeType: string; data: string; size: number } }
}
expect(data.output.file.name).toBe('report.txt')
expect(data.output.file.mimeType).toBe('text/plain')
expect(data.output.file.size).toBe(fileBytes.length)
expect(Buffer.from(data.output.file.data, 'base64').toString('utf-8')).toBe('hello-attachment')
const calls = inputValidationMockFns.mockSecureFetchWithPinnedIP.mock.calls
expect(calls).toHaveLength(3)
// All three outbound calls must use the pre-resolved IP.
for (const call of calls) {
expect(call[1]).toBe(PINNED_IP)
}
// Original hostname is preserved in the URL (so TLS SNI works).
expect(calls[0][0]).toContain('https://example.agiloft.com/ewws/EWLogin')
expect(calls[1][0]).toContain('https://example.agiloft.com/ewws/EWRetrieve')
expect(calls[1][2]).toMatchObject({
method: 'GET',
headers: { Authorization: 'Bearer tok-xyz' },
})
expect(calls[2][0]).toContain('https://example.agiloft.com/ewws/EWLogout')
// DNS only resolved once — no second lookup that could rebind.
expect(inputValidationMockFns.mockValidateUrlWithDNS).toHaveBeenCalledTimes(1)
})
it('propagates upstream errors and still calls logout', async () => {
inputValidationMockFns.mockSecureFetchWithPinnedIP
.mockResolvedValueOnce(mockSecureFetchResponse({ json: { access_token: 'tok-err' } }))
.mockResolvedValueOnce(
mockSecureFetchResponse({ ok: false, status: 404, text: 'Record not found' })
)
.mockResolvedValueOnce(mockSecureFetchResponse({}))
const response = await POST(createMockRequest('POST', baseBody))
expect(response.status).toBe(404)
const data = (await response.json()) as { success: false; error: string }
expect(data.error).toContain('Record not found')
// Logout still runs.
expect(inputValidationMockFns.mockSecureFetchWithPinnedIP).toHaveBeenCalledTimes(3)
expect(inputValidationMockFns.mockSecureFetchWithPinnedIP.mock.calls[2][0]).toContain(
'/ewws/EWLogout'
)
})
})
@@ -0,0 +1,137 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { agiloftRetrieveContract } from '@/lib/api/contracts/tools/agiloft'
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { secureFetchWithPinnedIP } from '@/lib/core/security/input-validation.server'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { buildRetrieveAttachmentUrl } from '@/tools/agiloft/utils'
import {
agiloftLoginPinned,
agiloftLogoutPinned,
resolveAgiloftInstance,
} from '@/tools/agiloft/utils.server'
export const dynamic = 'force-dynamic'
const logger = createLogger('AgiloftRetrieveAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
logger.warn(`[${requestId}] Unauthorized Agiloft retrieve attempt: ${authResult.error}`)
return NextResponse.json(
{ success: false, error: authResult.error || 'Authentication required' },
{ status: 401 }
)
}
const parsed = await parseRequest(
agiloftRetrieveContract,
request,
{},
{
validationErrorResponse: (error) => {
logger.warn(`[${requestId}] Invalid request data`, { errors: error.issues })
return NextResponse.json(
{
success: false,
error: getValidationErrorMessage(error, 'Invalid request data'),
details: error.issues,
},
{ status: 400 }
)
},
}
)
if (!parsed.success) return parsed.response
const data = parsed.data.body
let resolvedIP: string
try {
resolvedIP = await resolveAgiloftInstance(data.instanceUrl)
} catch (error) {
logger.warn(`[${requestId}] SSRF attempt blocked for Agiloft instance URL`, {
instanceUrl: data.instanceUrl,
})
return NextResponse.json({ success: false, error: toError(error).message }, { status: 400 })
}
const token = await agiloftLoginPinned(data, resolvedIP)
const base = data.instanceUrl.replace(/\/$/, '')
try {
const url = buildRetrieveAttachmentUrl(base, data)
logger.info(`[${requestId}] Downloading attachment from Agiloft`, {
recordId: data.recordId,
fieldName: data.fieldName,
position: data.position,
})
const agiloftResponse = await secureFetchWithPinnedIP(url, resolvedIP, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
})
if (!agiloftResponse.ok) {
const errorText = await agiloftResponse.text()
logger.error(
`[${requestId}] Agiloft retrieve error: ${agiloftResponse.status} - ${errorText}`
)
return NextResponse.json(
{ success: false, error: `Agiloft error: ${agiloftResponse.status} - ${errorText}` },
{ status: agiloftResponse.status }
)
}
const contentType = agiloftResponse.headers.get('content-type') || 'application/octet-stream'
const contentDisposition = agiloftResponse.headers.get('content-disposition')
let fileName = 'attachment'
if (contentDisposition) {
const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/)
if (match?.[1]) {
fileName = match[1].replace(/['"]/g, '')
}
}
const arrayBuffer = await agiloftResponse.arrayBuffer()
const fileBuffer = Buffer.from(arrayBuffer)
logger.info(`[${requestId}] Attachment downloaded successfully`, {
name: fileName,
size: fileBuffer.length,
mimeType: contentType,
})
const base64Data = fileBuffer.toString('base64')
return NextResponse.json({
success: true,
output: {
file: {
name: fileName,
mimeType: contentType,
data: base64Data,
size: fileBuffer.length,
},
},
})
} finally {
await agiloftLogoutPinned(data.instanceUrl, data.knowledgeBase, token, resolvedIP)
}
} catch (error) {
logger.error(`[${requestId}] Error retrieving Agiloft attachment:`, error)
return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 })
}
})
@@ -0,0 +1,104 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { agiloftSavedSearchContract } from '@/lib/api/contracts/tools/agiloft'
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import type { AgiloftSavedSearchResponse } from '@/tools/agiloft/types'
import { buildSavedSearchUrl } from '@/tools/agiloft/utils'
import { executeAgiloftRequest } from '@/tools/agiloft/utils.server'
export const dynamic = 'force-dynamic'
const logger = createLogger('AgiloftSavedSearchAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Agiloft saved_search attempt: ${authResult.error}`)
return NextResponse.json(
{ success: false, error: authResult.error || 'Authentication required' },
{ status: 401 }
)
}
const parsed = await parseRequest(
agiloftSavedSearchContract,
request,
{},
{
validationErrorResponse: (error) => {
logger.warn(`[${requestId}] Invalid request data`, { errors: error.issues })
return NextResponse.json(
{
success: false,
error: getValidationErrorMessage(error, 'Invalid request data'),
details: error.issues,
},
{ status: 400 }
)
},
}
)
if (!parsed.success) return parsed.response
const params = parsed.data.body
const result = await executeAgiloftRequest<AgiloftSavedSearchResponse>(
params,
(base) => ({
url: buildSavedSearchUrl(base, params),
method: 'GET',
}),
async (response) => {
if (!response.ok) {
const errorText = await response.text()
return {
success: false,
output: { searches: [] },
error: `Agiloft error: ${response.status} - ${errorText}`,
}
}
const data = (await response.json()) as Record<string, unknown>
const result = (data.result ?? data) as Record<string, unknown>
const searches: Array<{
name: string
label: string
id: string | number
description: string | null
}> = []
if (Array.isArray(result)) {
for (const item of result as Record<string, unknown>[]) {
searches.push({
name: (item.name as string) ?? '',
label: (item.label as string) ?? (item.name as string) ?? '',
id: (item.id as string | number) ?? (item.ID as string | number) ?? '',
description: (item.description as string | null) ?? null,
})
}
}
return {
success: data.success !== false,
output: {
searches,
},
}
}
)
return NextResponse.json(result)
} catch (error) {
logger.error(`[${requestId}] Error listing Agiloft saved searches:`, error)
return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 })
}
})
@@ -0,0 +1,129 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { agiloftSearchRecordsContract } from '@/lib/api/contracts/tools/agiloft'
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import type { AgiloftSearchResponse } from '@/tools/agiloft/types'
import { buildSearchRecordsUrl } from '@/tools/agiloft/utils'
import { executeAgiloftRequest } from '@/tools/agiloft/utils.server'
export const dynamic = 'force-dynamic'
const logger = createLogger('AgiloftSearchRecordsAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Agiloft search_records attempt: ${authResult.error}`)
return NextResponse.json(
{ success: false, error: authResult.error || 'Authentication required' },
{ status: 401 }
)
}
const parsed = await parseRequest(
agiloftSearchRecordsContract,
request,
{},
{
validationErrorResponse: (error) => {
logger.warn(`[${requestId}] Invalid request data`, { errors: error.issues })
return NextResponse.json(
{
success: false,
error: getValidationErrorMessage(error, 'Invalid request data'),
details: error.issues,
},
{ status: 400 }
)
},
}
)
if (!parsed.success) return parsed.response
const params = parsed.data.body
const result = await executeAgiloftRequest<AgiloftSearchResponse>(
params,
(base) => ({
url: buildSearchRecordsUrl(base, params),
method: 'GET',
}),
async (response) => {
if (!response.ok) {
const errorText = await response.text()
return {
success: false,
output: { records: [], totalCount: 0, page: 0, limit: 25 },
error: `Agiloft error: ${response.status} - ${errorText}`,
}
}
const data = (await response.json()) as Record<string, unknown>
const records: Record<string, unknown>[] = []
const result = (data.result ?? data) as Record<string, unknown>
if (Array.isArray(result)) {
for (const item of result as Record<string, unknown>[]) {
records.push(item)
}
} else {
const lengthRaw = result.EWREST_length ?? data.EWREST_length
const count = typeof lengthRaw === 'string' ? Number(lengthRaw) : (lengthRaw as number)
if (typeof count === 'number' && Number.isFinite(count)) {
const source = (result.EWREST_length != null ? result : data) as Record<string, unknown>
for (let i = 0; i < count; i++) {
const record: Record<string, unknown> = {}
for (const key of Object.keys(source)) {
const match = key.match(/^EWREST_(.+)_(\d+)$/)
if (match && Number(match[2]) === i) {
record[match[1]] = source[key]
}
}
if (Object.keys(record).length > 0) {
records.push(record)
}
}
}
}
const totalCountRaw =
result.totalCount ??
result.total ??
result.count ??
result.EWREST_length ??
data.totalCount ??
data.total ??
data.count ??
data.EWREST_length ??
records.length
const totalCount =
typeof totalCountRaw === 'string' ? Number(totalCountRaw) : (totalCountRaw as number)
const page = params.page ? Number(params.page) : 0
const limit = params.limit ? Number(params.limit) : 25
return {
success: data.success !== false,
output: {
records,
totalCount,
page,
limit,
},
}
}
)
return NextResponse.json(result)
} catch (error) {
logger.error(`[${requestId}] Error searching Agiloft records:`, error)
return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 })
}
})
@@ -0,0 +1,116 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { agiloftSelectRecordsContract } from '@/lib/api/contracts/tools/agiloft'
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import type { AgiloftSelectResponse } from '@/tools/agiloft/types'
import { buildSelectRecordsUrl } from '@/tools/agiloft/utils'
import { executeAgiloftRequest } from '@/tools/agiloft/utils.server'
export const dynamic = 'force-dynamic'
const logger = createLogger('AgiloftSelectRecordsAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Agiloft select_records attempt: ${authResult.error}`)
return NextResponse.json(
{ success: false, error: authResult.error || 'Authentication required' },
{ status: 401 }
)
}
const parsed = await parseRequest(
agiloftSelectRecordsContract,
request,
{},
{
validationErrorResponse: (error) => {
logger.warn(`[${requestId}] Invalid request data`, { errors: error.issues })
return NextResponse.json(
{
success: false,
error: getValidationErrorMessage(error, 'Invalid request data'),
details: error.issues,
},
{ status: 400 }
)
},
}
)
if (!parsed.success) return parsed.response
const params = parsed.data.body
const result = await executeAgiloftRequest<AgiloftSelectResponse>(
params,
(base) => ({
url: buildSelectRecordsUrl(base, params),
method: 'GET',
}),
async (response) => {
if (!response.ok) {
const errorText = await response.text()
return {
success: false,
output: { recordIds: [], totalCount: 0 },
error: `Agiloft error: ${response.status} - ${errorText}`,
}
}
const data = (await response.json()) as Record<string, unknown>
const result = (data.result ?? data) as Record<string, unknown>
const recordIds: string[] = []
if (Array.isArray(result)) {
for (const item of result as Record<string, unknown>[]) {
const id = item.id ?? item.ID ?? item
recordIds.push(String(id))
}
} else if (typeof result === 'object' && result !== null) {
let i = 0
while (result[`id_${i}`] !== undefined || result[`EWREST_id_${i}`] !== undefined) {
const id = result[`id_${i}`] ?? result[`EWREST_id_${i}`]
recordIds.push(String(id))
i++
}
if (recordIds.length === 0 && result.id !== undefined) {
recordIds.push(String(result.id))
}
}
const totalCountRaw =
result.EWREST_id_length ??
result.totalCount ??
result.total ??
result.count ??
data.EWREST_id_length ??
data.totalCount ??
data.total ??
data.count ??
recordIds.length
return {
success: data.success !== false,
output: {
recordIds,
totalCount: Number(totalCountRaw),
},
}
}
)
return NextResponse.json(result)
} catch (error) {
logger.error(`[${requestId}] Error selecting Agiloft records:`, error)
return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 })
}
})
@@ -0,0 +1,101 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { agiloftUpdateRecordContract } from '@/lib/api/contracts/tools/agiloft'
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import type { AgiloftRecordResponse } from '@/tools/agiloft/types'
import { buildUpdateRecordUrl } from '@/tools/agiloft/utils'
import { executeAgiloftRequest } from '@/tools/agiloft/utils.server'
export const dynamic = 'force-dynamic'
const logger = createLogger('AgiloftUpdateRecordAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Agiloft update_record attempt: ${authResult.error}`)
return NextResponse.json(
{ success: false, error: authResult.error || 'Authentication required' },
{ status: 401 }
)
}
const parsed = await parseRequest(
agiloftUpdateRecordContract,
request,
{},
{
validationErrorResponse: (error) => {
logger.warn(`[${requestId}] Invalid request data`, { errors: error.issues })
return NextResponse.json(
{
success: false,
error: getValidationErrorMessage(error, 'Invalid request data'),
details: error.issues,
},
{ status: 400 }
)
},
}
)
if (!parsed.success) return parsed.response
const params = parsed.data.body
let body: string
try {
body = JSON.stringify(JSON.parse(params.data))
} catch {
return NextResponse.json({
success: false,
output: { id: null, fields: {} },
error: 'Invalid JSON in data parameter',
})
}
const result = await executeAgiloftRequest<AgiloftRecordResponse>(
params,
(base) => ({
url: buildUpdateRecordUrl(base, params),
method: 'PUT',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body,
}),
async (response) => {
if (!response.ok) {
const errorText = await response.text()
return {
success: false,
output: { id: null, fields: {} },
error: `Agiloft error: ${response.status} - ${errorText}`,
}
}
const data = (await response.json()) as Record<string, unknown>
const result = (data.result ?? data) as Record<string, unknown>
const id = result.id ?? result.ID ?? data.id ?? data.ID ?? null
return {
success: data.success !== false,
output: {
id: id != null ? String(id) : null,
fields: result ?? {},
},
}
}
)
return NextResponse.json(result)
} catch (error) {
logger.error(`[${requestId}] Error updating Agiloft record:`, error)
return NextResponse.json({ success: false, error: toError(error).message }, { status: 500 })
}
})
@@ -0,0 +1,143 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { airtableBasesSelectorContract } from '@/lib/api/contracts/selectors'
import { parseRequest } from '@/lib/api/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
const logger = createLogger('AirtableBasesAPI')
export const dynamic = 'force-dynamic'
const AIRTABLE_MAX_BASES_PAGES = 50
interface AirtableBase {
id: string
name: string
}
/**
* Lists all Airtable bases, following the `offset` continuation token the Meta
* API returns (an opaque string, passed back verbatim as `?offset=`) so the
* full set is returned. Bounded by `AIRTABLE_MAX_BASES_PAGES`; logs a warning
* rather than silently dropping bases when the cap is hit.
*/
async function fetchAllBases(accessToken: string): Promise<AirtableBase[]> {
const bases: AirtableBase[] = []
let offset: string | undefined
for (let page = 0; page < AIRTABLE_MAX_BASES_PAGES; page++) {
const url = new URL('https://api.airtable.com/v0/meta/bases')
if (offset) {
url.searchParams.set('offset', offset)
}
const response = await fetch(url.toString(), {
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
throw new AirtableFetchError(response.status, errorData)
}
const data = (await response.json()) as { bases?: AirtableBase[]; offset?: string }
if (Array.isArray(data.bases)) {
bases.push(...data.bases)
}
offset = data.offset || undefined
if (!offset) {
return bases
}
if (page === AIRTABLE_MAX_BASES_PAGES - 1) {
logger.warn('Airtable bases listing hit pagination cap; base list may be incomplete', {
pages: AIRTABLE_MAX_BASES_PAGES,
})
}
}
return bases
}
class AirtableFetchError extends Error {
constructor(
readonly status: number,
readonly details: unknown
) {
super('Failed to fetch Airtable bases')
this.name = 'AirtableFetchError'
}
}
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const parsed = await parseRequest(airtableBasesSelectorContract, request, {})
if (!parsed.success) {
logger.error('Missing credential in request')
return parsed.response
}
const { credential, workflowId } = parsed.data.body
const authz = await authorizeCredentialUse(request, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{ error: 'Could not retrieve access token', authRequired: true },
{ status: 401 }
)
}
let allBases: AirtableBase[]
try {
allBases = await fetchAllBases(accessToken)
} catch (error) {
if (error instanceof AirtableFetchError) {
logger.error('Failed to fetch Airtable bases', {
status: error.status,
error: error.details,
})
return NextResponse.json(
{ error: 'Failed to fetch Airtable bases', details: error.details },
{ status: error.status }
)
}
throw error
}
const bases = allBases.map((base) => ({
id: base.id,
name: base.name,
}))
return NextResponse.json({ bases })
} catch (error) {
logger.error('Error processing Airtable bases request:', error)
return NextResponse.json(
{ error: 'Failed to retrieve Airtable bases', details: (error as Error).message },
{ status: 500 }
)
}
})
@@ -0,0 +1,89 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { airtableTablesSelectorContract } from '@/lib/api/contracts/selectors'
import { parseRequest } from '@/lib/api/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { validateAirtableId } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
const logger = createLogger('AirtableTablesAPI')
export const dynamic = 'force-dynamic'
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const parsed = await parseRequest(airtableTablesSelectorContract, request, {})
if (!parsed.success) return parsed.response
const { credential, workflowId, baseId } = parsed.data.body
const baseIdValidation = validateAirtableId(baseId, 'app', 'baseId')
if (!baseIdValidation.isValid) {
logger.error('Invalid baseId', { error: baseIdValidation.error })
return NextResponse.json({ error: baseIdValidation.error }, { status: 400 })
}
const authz = await authorizeCredentialUse(request, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{ error: 'Could not retrieve access token', authRequired: true },
{ status: 401 }
)
}
const response = await fetch(
`https://api.airtable.com/v0/meta/bases/${baseIdValidation.sanitized}/tables`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
}
)
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
logger.error('Failed to fetch Airtable tables', {
status: response.status,
error: errorData,
baseId,
})
return NextResponse.json(
{ error: 'Failed to fetch Airtable tables', details: errorData },
{ status: response.status }
)
}
const data = await response.json()
const tables = (data.tables || []).map((table: { id: string; name: string }) => ({
id: table.id,
name: table.name,
}))
return NextResponse.json({ tables })
} catch (error) {
logger.error('Error processing Airtable tables request:', error)
return NextResponse.json(
{ error: 'Failed to retrieve Airtable tables', details: (error as Error).message },
{ status: 500 }
)
}
})
@@ -0,0 +1,52 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAppConfigCreateApplicationContract } from '@/lib/api/contracts/tools/aws/appconfig-create-application'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAppConfigClient, createApplication } from '../utils'
const logger = createLogger('AppConfigCreateApplicationAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseToolRequest(awsAppConfigCreateApplicationContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(`[${requestId}] Creating AppConfig application ${params.name}`)
const client = createAppConfigClient({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
})
try {
const result = await createApplication(client, params.name, params.description)
logger.info(`[${requestId}] Created application ${result.id}`)
return NextResponse.json(result)
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] Failed to create application:`, error)
return NextResponse.json(
{ error: `Failed to create application: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,60 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAppConfigCreateConfigurationProfileContract } from '@/lib/api/contracts/tools/aws/appconfig-create-configuration-profile'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAppConfigClient, createConfigurationProfile } from '../utils'
const logger = createLogger('AppConfigCreateConfigurationProfileAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseToolRequest(awsAppConfigCreateConfigurationProfileContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(`[${requestId}] Creating AppConfig configuration profile ${params.name}`)
const client = createAppConfigClient({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
})
try {
const result = await createConfigurationProfile(
client,
params.applicationId,
params.name,
params.locationUri,
params.description,
params.retrievalRoleArn,
params.type
)
logger.info(`[${requestId}] Created configuration profile ${result.id}`)
return NextResponse.json(result)
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] Failed to create configuration profile:`, error)
return NextResponse.json(
{ error: `Failed to create configuration profile: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,57 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAppConfigCreateEnvironmentContract } from '@/lib/api/contracts/tools/aws/appconfig-create-environment'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAppConfigClient, createEnvironment } from '../utils'
const logger = createLogger('AppConfigCreateEnvironmentAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseToolRequest(awsAppConfigCreateEnvironmentContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(`[${requestId}] Creating AppConfig environment ${params.name}`)
const client = createAppConfigClient({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
})
try {
const result = await createEnvironment(
client,
params.applicationId,
params.name,
params.description
)
logger.info(`[${requestId}] Created environment ${result.id}`)
return NextResponse.json(result)
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] Failed to create environment:`, error)
return NextResponse.json(
{ error: `Failed to create environment: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,64 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAppConfigCreateHostedConfigurationVersionContract } from '@/lib/api/contracts/tools/aws/appconfig-create-hosted-configuration-version'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAppConfigClient, createHostedConfigurationVersion } from '../utils'
const logger = createLogger('AppConfigCreateHostedConfigurationVersionAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseToolRequest(
awsAppConfigCreateHostedConfigurationVersionContract,
request,
{ errorFormat: 'details', logger }
)
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(
`[${requestId}] Creating hosted configuration version for profile ${params.configurationProfileId}`
)
const client = createAppConfigClient({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
})
try {
const result = await createHostedConfigurationVersion(
client,
params.applicationId,
params.configurationProfileId,
params.content,
params.contentType,
params.description,
params.latestVersionNumber,
params.versionLabel
)
logger.info(`[${requestId}] Created hosted configuration version ${result.versionNumber}`)
return NextResponse.json(result)
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] Failed to create hosted configuration version:`, error)
return NextResponse.json(
{ error: `Failed to create hosted configuration version: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,52 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAppConfigDeleteApplicationContract } from '@/lib/api/contracts/tools/aws/appconfig-delete-application'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAppConfigClient, deleteApplication } from '../utils'
const logger = createLogger('AppConfigDeleteApplicationAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseToolRequest(awsAppConfigDeleteApplicationContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(`[${requestId}] Deleting AppConfig application ${params.applicationId}`)
const client = createAppConfigClient({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
})
try {
const result = await deleteApplication(client, params.applicationId)
logger.info(`[${requestId}] Deleted application`)
return NextResponse.json(result)
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] Failed to delete application:`, error)
return NextResponse.json(
{ error: `Failed to delete application: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,58 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAppConfigDeleteConfigurationProfileContract } from '@/lib/api/contracts/tools/aws/appconfig-delete-configuration-profile'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAppConfigClient, deleteConfigurationProfile } from '../utils'
const logger = createLogger('AppConfigDeleteConfigurationProfileAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseToolRequest(awsAppConfigDeleteConfigurationProfileContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(
`[${requestId}] Deleting AppConfig configuration profile ${params.configurationProfileId}`
)
const client = createAppConfigClient({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
})
try {
const result = await deleteConfigurationProfile(
client,
params.applicationId,
params.configurationProfileId
)
logger.info(`[${requestId}] Deleted configuration profile`)
return NextResponse.json(result)
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] Failed to delete configuration profile:`, error)
return NextResponse.json(
{ error: `Failed to delete configuration profile: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,52 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAppConfigDeleteEnvironmentContract } from '@/lib/api/contracts/tools/aws/appconfig-delete-environment'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAppConfigClient, deleteEnvironment } from '../utils'
const logger = createLogger('AppConfigDeleteEnvironmentAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseToolRequest(awsAppConfigDeleteEnvironmentContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(`[${requestId}] Deleting AppConfig environment ${params.environmentId}`)
const client = createAppConfigClient({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
})
try {
const result = await deleteEnvironment(client, params.applicationId, params.environmentId)
logger.info(`[${requestId}] Deleted environment`)
return NextResponse.json(result)
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] Failed to delete environment:`, error)
return NextResponse.json(
{ error: `Failed to delete environment: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,60 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAppConfigDeleteHostedConfigurationVersionContract } from '@/lib/api/contracts/tools/aws/appconfig-delete-hosted-configuration-version'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAppConfigClient, deleteHostedConfigurationVersion } from '../utils'
const logger = createLogger('AppConfigDeleteHostedConfigurationVersionAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseToolRequest(
awsAppConfigDeleteHostedConfigurationVersionContract,
request,
{ errorFormat: 'details', logger }
)
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(
`[${requestId}] Deleting hosted configuration version ${params.versionNumber} for profile ${params.configurationProfileId}`
)
const client = createAppConfigClient({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
})
try {
const result = await deleteHostedConfigurationVersion(
client,
params.applicationId,
params.configurationProfileId,
params.versionNumber
)
logger.info(`[${requestId}] Deleted hosted configuration version`)
return NextResponse.json(result)
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] Failed to delete hosted configuration version:`, error)
return NextResponse.json(
{ error: `Failed to delete hosted configuration version: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,52 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAppConfigGetApplicationContract } from '@/lib/api/contracts/tools/aws/appconfig-get-application'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAppConfigClient, getApplication } from '../utils'
const logger = createLogger('AppConfigGetApplicationAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseToolRequest(awsAppConfigGetApplicationContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(`[${requestId}] Getting AppConfig application ${params.applicationId}`)
const client = createAppConfigClient({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
})
try {
const result = await getApplication(client, params.applicationId)
logger.info(`[${requestId}] Retrieved application`)
return NextResponse.json(result)
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] Failed to get application:`, error)
return NextResponse.json(
{ error: `Failed to get application: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,58 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAppConfigGetConfigurationProfileContract } from '@/lib/api/contracts/tools/aws/appconfig-get-configuration-profile'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAppConfigClient, getConfigurationProfile } from '../utils'
const logger = createLogger('AppConfigGetConfigurationProfileAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseToolRequest(awsAppConfigGetConfigurationProfileContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(
`[${requestId}] Getting AppConfig configuration profile ${params.configurationProfileId}`
)
const client = createAppConfigClient({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
})
try {
const result = await getConfigurationProfile(
client,
params.applicationId,
params.configurationProfileId
)
logger.info(`[${requestId}] Retrieved configuration profile`)
return NextResponse.json(result)
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] Failed to get configuration profile:`, error)
return NextResponse.json(
{ error: `Failed to get configuration profile: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,59 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAppConfigGetConfigurationContract } from '@/lib/api/contracts/tools/aws/appconfig-get-configuration'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAppConfigDataClient, getConfiguration } from '../utils'
const logger = createLogger('AppConfigGetConfigurationAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseToolRequest(awsAppConfigGetConfigurationContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(
`[${requestId}] Retrieving AppConfig configuration for ${params.applicationId}/${params.environmentId}/${params.configurationProfileId}`
)
const client = createAppConfigDataClient({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
})
try {
const result = await getConfiguration(
client,
params.applicationId,
params.environmentId,
params.configurationProfileId
)
logger.info(`[${requestId}] Retrieved configuration`)
return NextResponse.json(result)
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] Failed to retrieve configuration:`, error)
return NextResponse.json(
{ error: `Failed to retrieve configuration: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,59 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAppConfigGetDeploymentContract } from '@/lib/api/contracts/tools/aws/appconfig-get-deployment'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAppConfigClient, getDeployment } from '../utils'
const logger = createLogger('AppConfigGetDeploymentAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseToolRequest(awsAppConfigGetDeploymentContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(
`[${requestId}] Getting AppConfig deployment ${params.deploymentNumber} in env ${params.environmentId}`
)
const client = createAppConfigClient({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
})
try {
const result = await getDeployment(
client,
params.applicationId,
params.environmentId,
params.deploymentNumber
)
logger.info(`[${requestId}] Retrieved deployment`)
return NextResponse.json(result)
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] Failed to get deployment:`, error)
return NextResponse.json(
{ error: `Failed to get deployment: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,52 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAppConfigGetEnvironmentContract } from '@/lib/api/contracts/tools/aws/appconfig-get-environment'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAppConfigClient, getEnvironment } from '../utils'
const logger = createLogger('AppConfigGetEnvironmentAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseToolRequest(awsAppConfigGetEnvironmentContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(`[${requestId}] Getting AppConfig environment ${params.environmentId}`)
const client = createAppConfigClient({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
})
try {
const result = await getEnvironment(client, params.applicationId, params.environmentId)
logger.info(`[${requestId}] Retrieved environment`)
return NextResponse.json(result)
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] Failed to get environment:`, error)
return NextResponse.json(
{ error: `Failed to get environment: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,60 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAppConfigGetHostedConfigurationVersionContract } from '@/lib/api/contracts/tools/aws/appconfig-get-hosted-configuration-version'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAppConfigClient, getHostedConfigurationVersion } from '../utils'
const logger = createLogger('AppConfigGetHostedConfigurationVersionAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseToolRequest(
awsAppConfigGetHostedConfigurationVersionContract,
request,
{ errorFormat: 'details', logger }
)
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(
`[${requestId}] Getting hosted configuration version ${params.versionNumber} for profile ${params.configurationProfileId}`
)
const client = createAppConfigClient({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
})
try {
const result = await getHostedConfigurationVersion(
client,
params.applicationId,
params.configurationProfileId,
params.versionNumber
)
logger.info(`[${requestId}] Retrieved hosted configuration version`)
return NextResponse.json(result)
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] Failed to get hosted configuration version:`, error)
return NextResponse.json(
{ error: `Failed to get hosted configuration version: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,52 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAppConfigListApplicationsContract } from '@/lib/api/contracts/tools/aws/appconfig-list-applications'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAppConfigClient, listApplications } from '../utils'
const logger = createLogger('AppConfigListApplicationsAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseToolRequest(awsAppConfigListApplicationsContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(`[${requestId}] Listing AppConfig applications`)
const client = createAppConfigClient({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
})
try {
const result = await listApplications(client, params.maxResults, params.nextToken)
logger.info(`[${requestId}] Listed ${result.count} applications`)
return NextResponse.json(result)
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] Failed to list applications:`, error)
return NextResponse.json(
{ error: `Failed to list applications: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,59 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAppConfigListConfigurationProfilesContract } from '@/lib/api/contracts/tools/aws/appconfig-list-configuration-profiles'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAppConfigClient, listConfigurationProfiles } from '../utils'
const logger = createLogger('AppConfigListConfigurationProfilesAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseToolRequest(awsAppConfigListConfigurationProfilesContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(
`[${requestId}] Listing AppConfig configuration profiles for ${params.applicationId}`
)
const client = createAppConfigClient({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
})
try {
const result = await listConfigurationProfiles(
client,
params.applicationId,
params.maxResults,
params.nextToken
)
logger.info(`[${requestId}] Listed ${result.count} configuration profiles`)
return NextResponse.json(result)
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] Failed to list configuration profiles:`, error)
return NextResponse.json(
{ error: `Failed to list configuration profiles: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,52 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAppConfigListDeploymentStrategiesContract } from '@/lib/api/contracts/tools/aws/appconfig-list-deployment-strategies'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAppConfigClient, listDeploymentStrategies } from '../utils'
const logger = createLogger('AppConfigListDeploymentStrategiesAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseToolRequest(awsAppConfigListDeploymentStrategiesContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(`[${requestId}] Listing AppConfig deployment strategies`)
const client = createAppConfigClient({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
})
try {
const result = await listDeploymentStrategies(client, params.maxResults, params.nextToken)
logger.info(`[${requestId}] Listed ${result.count} deployment strategies`)
return NextResponse.json(result)
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] Failed to list deployment strategies:`, error)
return NextResponse.json(
{ error: `Failed to list deployment strategies: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,58 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAppConfigListDeploymentsContract } from '@/lib/api/contracts/tools/aws/appconfig-list-deployments'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAppConfigClient, listDeployments } from '../utils'
const logger = createLogger('AppConfigListDeploymentsAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseToolRequest(awsAppConfigListDeploymentsContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(`[${requestId}] Listing AppConfig deployments in env ${params.environmentId}`)
const client = createAppConfigClient({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
})
try {
const result = await listDeployments(
client,
params.applicationId,
params.environmentId,
params.maxResults,
params.nextToken
)
logger.info(`[${requestId}] Listed ${result.count} deployments`)
return NextResponse.json(result)
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] Failed to list deployments:`, error)
return NextResponse.json(
{ error: `Failed to list deployments: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,57 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAppConfigListEnvironmentsContract } from '@/lib/api/contracts/tools/aws/appconfig-list-environments'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAppConfigClient, listEnvironments } from '../utils'
const logger = createLogger('AppConfigListEnvironmentsAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseToolRequest(awsAppConfigListEnvironmentsContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(`[${requestId}] Listing AppConfig environments for ${params.applicationId}`)
const client = createAppConfigClient({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
})
try {
const result = await listEnvironments(
client,
params.applicationId,
params.maxResults,
params.nextToken
)
logger.info(`[${requestId}] Listed ${result.count} environments`)
return NextResponse.json(result)
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] Failed to list environments:`, error)
return NextResponse.json(
{ error: `Failed to list environments: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,61 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAppConfigListHostedConfigurationVersionsContract } from '@/lib/api/contracts/tools/aws/appconfig-list-hosted-configuration-versions'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAppConfigClient, listHostedConfigurationVersions } from '../utils'
const logger = createLogger('AppConfigListHostedConfigurationVersionsAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseToolRequest(
awsAppConfigListHostedConfigurationVersionsContract,
request,
{ errorFormat: 'details', logger }
)
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(
`[${requestId}] Listing AppConfig hosted configuration versions for profile ${params.configurationProfileId}`
)
const client = createAppConfigClient({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
})
try {
const result = await listHostedConfigurationVersions(
client,
params.applicationId,
params.configurationProfileId,
params.maxResults,
params.nextToken
)
logger.info(`[${requestId}] Listed ${result.count} hosted configuration versions`)
return NextResponse.json(result)
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] Failed to list hosted configuration versions:`, error)
return NextResponse.json(
{ error: `Failed to list hosted configuration versions: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,60 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAppConfigStartDeploymentContract } from '@/lib/api/contracts/tools/aws/appconfig-start-deployment'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAppConfigClient, startDeployment } from '../utils'
const logger = createLogger('AppConfigStartDeploymentAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseToolRequest(awsAppConfigStartDeploymentContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(`[${requestId}] Starting AppConfig deployment in env ${params.environmentId}`)
const client = createAppConfigClient({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
})
try {
const result = await startDeployment(
client,
params.applicationId,
params.environmentId,
params.deploymentStrategyId,
params.configurationProfileId,
params.configurationVersion,
params.description
)
logger.info(`[${requestId}] Started deployment ${result.deploymentNumber}`)
return NextResponse.json(result)
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] Failed to start deployment:`, error)
return NextResponse.json(
{ error: `Failed to start deployment: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,59 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAppConfigStopDeploymentContract } from '@/lib/api/contracts/tools/aws/appconfig-stop-deployment'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAppConfigClient, stopDeployment } from '../utils'
const logger = createLogger('AppConfigStopDeploymentAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseToolRequest(awsAppConfigStopDeploymentContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(
`[${requestId}] Stopping AppConfig deployment ${params.deploymentNumber} in env ${params.environmentId}`
)
const client = createAppConfigClient({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
})
try {
const result = await stopDeployment(
client,
params.applicationId,
params.environmentId,
params.deploymentNumber
)
logger.info(`[${requestId}] Stopped deployment ${result.deploymentNumber}`)
return NextResponse.json(result)
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] Failed to stop deployment:`, error)
return NextResponse.json(
{ error: `Failed to stop deployment: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,57 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAppConfigUpdateApplicationContract } from '@/lib/api/contracts/tools/aws/appconfig-update-application'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAppConfigClient, updateApplication } from '../utils'
const logger = createLogger('AppConfigUpdateApplicationAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseToolRequest(awsAppConfigUpdateApplicationContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(`[${requestId}] Updating AppConfig application ${params.applicationId}`)
const client = createAppConfigClient({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
})
try {
const result = await updateApplication(
client,
params.applicationId,
params.name,
params.description
)
logger.info(`[${requestId}] Updated application`)
return NextResponse.json(result)
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] Failed to update application:`, error)
return NextResponse.json(
{ error: `Failed to update application: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,61 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAppConfigUpdateConfigurationProfileContract } from '@/lib/api/contracts/tools/aws/appconfig-update-configuration-profile'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAppConfigClient, updateConfigurationProfile } from '../utils'
const logger = createLogger('AppConfigUpdateConfigurationProfileAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseToolRequest(awsAppConfigUpdateConfigurationProfileContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(
`[${requestId}] Updating AppConfig configuration profile ${params.configurationProfileId}`
)
const client = createAppConfigClient({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
})
try {
const result = await updateConfigurationProfile(
client,
params.applicationId,
params.configurationProfileId,
params.name,
params.description,
params.retrievalRoleArn
)
logger.info(`[${requestId}] Updated configuration profile`)
return NextResponse.json(result)
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] Failed to update configuration profile:`, error)
return NextResponse.json(
{ error: `Failed to update configuration profile: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,58 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAppConfigUpdateEnvironmentContract } from '@/lib/api/contracts/tools/aws/appconfig-update-environment'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAppConfigClient, updateEnvironment } from '../utils'
const logger = createLogger('AppConfigUpdateEnvironmentAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseToolRequest(awsAppConfigUpdateEnvironmentContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(`[${requestId}] Updating AppConfig environment ${params.environmentId}`)
const client = createAppConfigClient({
region: params.region,
accessKeyId: params.accessKeyId,
secretAccessKey: params.secretAccessKey,
})
try {
const result = await updateEnvironment(
client,
params.applicationId,
params.environmentId,
params.name,
params.description
)
logger.info(`[${requestId}] Updated environment`)
return NextResponse.json(result)
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] Failed to update environment:`, error)
return NextResponse.json(
{ error: `Failed to update environment: ${errorMessage}` },
{ status: 500 }
)
}
})
+681
View File
@@ -0,0 +1,681 @@
import {
AppConfigClient,
CreateApplicationCommand,
CreateConfigurationProfileCommand,
CreateEnvironmentCommand,
CreateHostedConfigurationVersionCommand,
DeleteApplicationCommand,
DeleteConfigurationProfileCommand,
DeleteEnvironmentCommand,
DeleteHostedConfigurationVersionCommand,
GetApplicationCommand,
GetConfigurationProfileCommand,
GetDeploymentCommand,
GetEnvironmentCommand,
GetHostedConfigurationVersionCommand,
ListApplicationsCommand,
ListConfigurationProfilesCommand,
ListDeploymentStrategiesCommand,
ListDeploymentsCommand,
ListEnvironmentsCommand,
ListHostedConfigurationVersionsCommand,
StartDeploymentCommand,
StopDeploymentCommand,
UpdateApplicationCommand,
UpdateConfigurationProfileCommand,
UpdateEnvironmentCommand,
} from '@aws-sdk/client-appconfig'
import {
AppConfigDataClient,
GetLatestConfigurationCommand,
StartConfigurationSessionCommand,
} from '@aws-sdk/client-appconfigdata'
import type { AppConfigConnectionConfig } from '@/tools/appconfig/types'
export function createAppConfigClient(config: AppConfigConnectionConfig): AppConfigClient {
return new AppConfigClient({
region: config.region,
credentials: {
accessKeyId: config.accessKeyId,
secretAccessKey: config.secretAccessKey,
},
})
}
export function createAppConfigDataClient(config: AppConfigConnectionConfig): AppConfigDataClient {
return new AppConfigDataClient({
region: config.region,
credentials: {
accessKeyId: config.accessKeyId,
secretAccessKey: config.secretAccessKey,
},
})
}
const textDecoder = new TextDecoder()
function decodeContent(content?: Uint8Array): string {
if (!content || content.length === 0) return ''
return textDecoder.decode(content)
}
export async function listApplications(
client: AppConfigClient,
maxResults?: number | null,
nextToken?: string | null
) {
const response = await client.send(
new ListApplicationsCommand({
...(maxResults ? { MaxResults: maxResults } : {}),
...(nextToken ? { NextToken: nextToken } : {}),
})
)
const applications = (response.Items ?? []).map((item) => ({
id: item.Id ?? '',
name: item.Name ?? '',
description: item.Description ?? null,
}))
return {
applications,
nextToken: response.NextToken ?? null,
count: applications.length,
}
}
export async function createApplication(
client: AppConfigClient,
name: string,
description?: string | null
) {
const response = await client.send(
new CreateApplicationCommand({
Name: name,
...(description ? { Description: description } : {}),
})
)
return {
message: `Application "${response.Name ?? name}" created`,
id: response.Id ?? '',
name: response.Name ?? '',
description: response.Description ?? null,
}
}
export async function listEnvironments(
client: AppConfigClient,
applicationId: string,
maxResults?: number | null,
nextToken?: string | null
) {
const response = await client.send(
new ListEnvironmentsCommand({
ApplicationId: applicationId,
...(maxResults ? { MaxResults: maxResults } : {}),
...(nextToken ? { NextToken: nextToken } : {}),
})
)
const environments = (response.Items ?? []).map((item) => ({
applicationId: item.ApplicationId ?? '',
id: item.Id ?? '',
name: item.Name ?? '',
description: item.Description ?? null,
state: item.State ?? null,
}))
return {
environments,
nextToken: response.NextToken ?? null,
count: environments.length,
}
}
export async function createEnvironment(
client: AppConfigClient,
applicationId: string,
name: string,
description?: string | null
) {
const response = await client.send(
new CreateEnvironmentCommand({
ApplicationId: applicationId,
Name: name,
...(description ? { Description: description } : {}),
})
)
return {
message: `Environment "${response.Name ?? name}" created`,
applicationId: response.ApplicationId ?? applicationId,
id: response.Id ?? '',
name: response.Name ?? '',
state: response.State ?? null,
}
}
export async function listConfigurationProfiles(
client: AppConfigClient,
applicationId: string,
maxResults?: number | null,
nextToken?: string | null
) {
const response = await client.send(
new ListConfigurationProfilesCommand({
ApplicationId: applicationId,
...(maxResults ? { MaxResults: maxResults } : {}),
...(nextToken ? { NextToken: nextToken } : {}),
})
)
const configurationProfiles = (response.Items ?? []).map((item) => ({
applicationId: item.ApplicationId ?? '',
id: item.Id ?? '',
name: item.Name ?? '',
description: null,
locationUri: item.LocationUri ?? null,
retrievalRoleArn: null,
type: item.Type ?? null,
validatorTypes: item.ValidatorTypes ?? [],
}))
return {
configurationProfiles,
nextToken: response.NextToken ?? null,
count: configurationProfiles.length,
}
}
export async function createConfigurationProfile(
client: AppConfigClient,
applicationId: string,
name: string,
locationUri: string,
description?: string | null,
retrievalRoleArn?: string | null,
type?: string | null
) {
const response = await client.send(
new CreateConfigurationProfileCommand({
ApplicationId: applicationId,
Name: name,
LocationUri: locationUri,
...(description ? { Description: description } : {}),
...(retrievalRoleArn ? { RetrievalRoleArn: retrievalRoleArn } : {}),
...(type ? { Type: type } : {}),
})
)
return {
message: `Configuration profile "${response.Name ?? name}" created`,
applicationId: response.ApplicationId ?? applicationId,
id: response.Id ?? '',
name: response.Name ?? '',
locationUri: response.LocationUri ?? null,
type: response.Type ?? null,
}
}
export async function createHostedConfigurationVersion(
client: AppConfigClient,
applicationId: string,
configurationProfileId: string,
content: string,
contentType: string,
description?: string | null,
latestVersionNumber?: number | null,
versionLabel?: string | null
) {
const response = await client.send(
new CreateHostedConfigurationVersionCommand({
ApplicationId: applicationId,
ConfigurationProfileId: configurationProfileId,
Content: new TextEncoder().encode(content),
ContentType: contentType,
...(description ? { Description: description } : {}),
...(latestVersionNumber != null ? { LatestVersionNumber: latestVersionNumber } : {}),
...(versionLabel ? { VersionLabel: versionLabel } : {}),
})
)
return {
message: `Hosted configuration version ${response.VersionNumber ?? ''} created`,
applicationId: response.ApplicationId ?? applicationId,
configurationProfileId: response.ConfigurationProfileId ?? configurationProfileId,
versionNumber: response.VersionNumber ?? null,
contentType: response.ContentType ?? null,
versionLabel: response.VersionLabel ?? null,
}
}
export async function getHostedConfigurationVersion(
client: AppConfigClient,
applicationId: string,
configurationProfileId: string,
versionNumber: number
) {
const response = await client.send(
new GetHostedConfigurationVersionCommand({
ApplicationId: applicationId,
ConfigurationProfileId: configurationProfileId,
VersionNumber: versionNumber,
})
)
return {
applicationId: response.ApplicationId ?? applicationId,
configurationProfileId: response.ConfigurationProfileId ?? configurationProfileId,
versionNumber: response.VersionNumber ?? null,
description: response.Description ?? null,
content: decodeContent(response.Content),
contentType: response.ContentType ?? null,
versionLabel: response.VersionLabel ?? null,
}
}
export async function listHostedConfigurationVersions(
client: AppConfigClient,
applicationId: string,
configurationProfileId: string,
maxResults?: number | null,
nextToken?: string | null
) {
const response = await client.send(
new ListHostedConfigurationVersionsCommand({
ApplicationId: applicationId,
ConfigurationProfileId: configurationProfileId,
...(maxResults ? { MaxResults: maxResults } : {}),
...(nextToken ? { NextToken: nextToken } : {}),
})
)
const versions = (response.Items ?? []).map((item) => ({
applicationId: item.ApplicationId ?? null,
configurationProfileId: item.ConfigurationProfileId ?? null,
versionNumber: item.VersionNumber ?? null,
description: item.Description ?? null,
contentType: item.ContentType ?? null,
versionLabel: item.VersionLabel ?? null,
}))
return {
versions,
nextToken: response.NextToken ?? null,
count: versions.length,
}
}
export async function listDeploymentStrategies(
client: AppConfigClient,
maxResults?: number | null,
nextToken?: string | null
) {
const response = await client.send(
new ListDeploymentStrategiesCommand({
...(maxResults ? { MaxResults: maxResults } : {}),
...(nextToken ? { NextToken: nextToken } : {}),
})
)
const deploymentStrategies = (response.Items ?? []).map((item) => ({
id: item.Id ?? '',
name: item.Name ?? '',
description: item.Description ?? null,
deploymentDurationInMinutes: item.DeploymentDurationInMinutes ?? null,
growthType: item.GrowthType ?? null,
growthFactor: item.GrowthFactor ?? null,
finalBakeTimeInMinutes: item.FinalBakeTimeInMinutes ?? null,
replicateTo: item.ReplicateTo ?? null,
}))
return {
deploymentStrategies,
nextToken: response.NextToken ?? null,
count: deploymentStrategies.length,
}
}
export async function startDeployment(
client: AppConfigClient,
applicationId: string,
environmentId: string,
deploymentStrategyId: string,
configurationProfileId: string,
configurationVersion: string,
description?: string | null
) {
const response = await client.send(
new StartDeploymentCommand({
ApplicationId: applicationId,
EnvironmentId: environmentId,
DeploymentStrategyId: deploymentStrategyId,
ConfigurationProfileId: configurationProfileId,
ConfigurationVersion: configurationVersion,
...(description ? { Description: description } : {}),
})
)
return {
message: `Deployment ${response.DeploymentNumber ?? ''} started`,
deploymentNumber: response.DeploymentNumber ?? null,
state: response.State ?? null,
percentageComplete: response.PercentageComplete ?? null,
}
}
export async function getDeployment(
client: AppConfigClient,
applicationId: string,
environmentId: string,
deploymentNumber: number
) {
const response = await client.send(
new GetDeploymentCommand({
ApplicationId: applicationId,
EnvironmentId: environmentId,
DeploymentNumber: deploymentNumber,
})
)
return {
applicationId: response.ApplicationId ?? applicationId,
environmentId: response.EnvironmentId ?? environmentId,
deploymentStrategyId: response.DeploymentStrategyId ?? '',
configurationProfileId: response.ConfigurationProfileId ?? '',
deploymentNumber: response.DeploymentNumber ?? null,
configurationName: response.ConfigurationName ?? null,
configurationVersion: response.ConfigurationVersion ?? null,
description: response.Description ?? null,
state: response.State ?? null,
percentageComplete: response.PercentageComplete ?? null,
startedAt: response.StartedAt?.toISOString() ?? null,
completedAt: response.CompletedAt?.toISOString() ?? null,
}
}
export async function listDeployments(
client: AppConfigClient,
applicationId: string,
environmentId: string,
maxResults?: number | null,
nextToken?: string | null
) {
const response = await client.send(
new ListDeploymentsCommand({
ApplicationId: applicationId,
EnvironmentId: environmentId,
...(maxResults ? { MaxResults: maxResults } : {}),
...(nextToken ? { NextToken: nextToken } : {}),
})
)
const deployments = (response.Items ?? []).map((item) => ({
deploymentNumber: item.DeploymentNumber ?? null,
configurationName: item.ConfigurationName ?? null,
configurationVersion: item.ConfigurationVersion ?? null,
deploymentDurationInMinutes: item.DeploymentDurationInMinutes ?? null,
growthType: item.GrowthType ?? null,
growthFactor: item.GrowthFactor ?? null,
finalBakeTimeInMinutes: item.FinalBakeTimeInMinutes ?? null,
state: item.State ?? null,
percentageComplete: item.PercentageComplete ?? null,
startedAt: item.StartedAt?.toISOString() ?? null,
completedAt: item.CompletedAt?.toISOString() ?? null,
versionLabel: item.VersionLabel ?? null,
}))
return {
deployments,
nextToken: response.NextToken ?? null,
count: deployments.length,
}
}
export async function stopDeployment(
client: AppConfigClient,
applicationId: string,
environmentId: string,
deploymentNumber: number
) {
const response = await client.send(
new StopDeploymentCommand({
ApplicationId: applicationId,
EnvironmentId: environmentId,
DeploymentNumber: deploymentNumber,
})
)
return {
message: `Deployment ${response.DeploymentNumber ?? deploymentNumber} stopped`,
deploymentNumber: response.DeploymentNumber ?? null,
state: response.State ?? null,
}
}
export async function getConfiguration(
client: AppConfigDataClient,
applicationId: string,
environmentId: string,
configurationProfileId: string
) {
const session = await client.send(
new StartConfigurationSessionCommand({
ApplicationIdentifier: applicationId,
EnvironmentIdentifier: environmentId,
ConfigurationProfileIdentifier: configurationProfileId,
})
)
const response = await client.send(
new GetLatestConfigurationCommand({
ConfigurationToken: session.InitialConfigurationToken,
})
)
return {
configuration: decodeContent(response.Configuration),
contentType: response.ContentType ?? null,
versionLabel: response.VersionLabel ?? null,
}
}
export async function getApplication(client: AppConfigClient, applicationId: string) {
const response = await client.send(new GetApplicationCommand({ ApplicationId: applicationId }))
return {
id: response.Id ?? '',
name: response.Name ?? '',
description: response.Description ?? null,
}
}
export async function updateApplication(
client: AppConfigClient,
applicationId: string,
name?: string | null,
description?: string | null
) {
const response = await client.send(
new UpdateApplicationCommand({
ApplicationId: applicationId,
...(name ? { Name: name } : {}),
...(description != null ? { Description: description } : {}),
})
)
return {
message: `Application "${response.Name ?? applicationId}" updated`,
id: response.Id ?? '',
name: response.Name ?? '',
description: response.Description ?? null,
}
}
export async function deleteApplication(client: AppConfigClient, applicationId: string) {
await client.send(new DeleteApplicationCommand({ ApplicationId: applicationId }))
return {
message: `Application ${applicationId} deleted`,
id: applicationId,
}
}
export async function getEnvironment(
client: AppConfigClient,
applicationId: string,
environmentId: string
) {
const response = await client.send(
new GetEnvironmentCommand({ ApplicationId: applicationId, EnvironmentId: environmentId })
)
return {
applicationId: response.ApplicationId ?? applicationId,
id: response.Id ?? '',
name: response.Name ?? '',
description: response.Description ?? null,
state: response.State ?? null,
monitors: (response.Monitors ?? []).map((monitor) => ({
alarmArn: monitor.AlarmArn ?? '',
alarmRoleArn: monitor.AlarmRoleArn ?? null,
})),
}
}
export async function updateEnvironment(
client: AppConfigClient,
applicationId: string,
environmentId: string,
name?: string | null,
description?: string | null
) {
const response = await client.send(
new UpdateEnvironmentCommand({
ApplicationId: applicationId,
EnvironmentId: environmentId,
...(name ? { Name: name } : {}),
...(description != null ? { Description: description } : {}),
})
)
return {
message: `Environment "${response.Name ?? environmentId}" updated`,
applicationId: response.ApplicationId ?? applicationId,
id: response.Id ?? '',
name: response.Name ?? '',
state: response.State ?? null,
}
}
export async function deleteEnvironment(
client: AppConfigClient,
applicationId: string,
environmentId: string
) {
await client.send(
new DeleteEnvironmentCommand({ ApplicationId: applicationId, EnvironmentId: environmentId })
)
return {
message: `Environment ${environmentId} deleted`,
applicationId,
id: environmentId,
}
}
export async function getConfigurationProfile(
client: AppConfigClient,
applicationId: string,
configurationProfileId: string
) {
const response = await client.send(
new GetConfigurationProfileCommand({
ApplicationId: applicationId,
ConfigurationProfileId: configurationProfileId,
})
)
return {
applicationId: response.ApplicationId ?? applicationId,
id: response.Id ?? '',
name: response.Name ?? '',
description: response.Description ?? null,
locationUri: response.LocationUri ?? null,
retrievalRoleArn: response.RetrievalRoleArn ?? null,
type: response.Type ?? null,
validators: (response.Validators ?? []).map((validator) => ({
type: validator.Type ?? '',
})),
}
}
export async function updateConfigurationProfile(
client: AppConfigClient,
applicationId: string,
configurationProfileId: string,
name?: string | null,
description?: string | null,
retrievalRoleArn?: string | null
) {
const response = await client.send(
new UpdateConfigurationProfileCommand({
ApplicationId: applicationId,
ConfigurationProfileId: configurationProfileId,
...(name ? { Name: name } : {}),
...(description != null ? { Description: description } : {}),
...(retrievalRoleArn != null ? { RetrievalRoleArn: retrievalRoleArn } : {}),
})
)
return {
message: `Configuration profile "${response.Name ?? configurationProfileId}" updated`,
applicationId: response.ApplicationId ?? applicationId,
id: response.Id ?? '',
name: response.Name ?? '',
description: response.Description ?? null,
type: response.Type ?? null,
}
}
export async function deleteConfigurationProfile(
client: AppConfigClient,
applicationId: string,
configurationProfileId: string
) {
await client.send(
new DeleteConfigurationProfileCommand({
ApplicationId: applicationId,
ConfigurationProfileId: configurationProfileId,
})
)
return {
message: `Configuration profile ${configurationProfileId} deleted`,
applicationId,
id: configurationProfileId,
}
}
export async function deleteHostedConfigurationVersion(
client: AppConfigClient,
applicationId: string,
configurationProfileId: string,
versionNumber: number
) {
await client.send(
new DeleteHostedConfigurationVersionCommand({
ApplicationId: applicationId,
ConfigurationProfileId: configurationProfileId,
VersionNumber: versionNumber,
})
)
return {
message: `Hosted configuration version ${versionNumber} deleted`,
applicationId,
configurationProfileId,
versionNumber,
}
}
@@ -0,0 +1,106 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { asanaAddCommentContract } from '@/lib/api/contracts/tools/asana'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
export const dynamic = 'force-dynamic'
const logger = createLogger('AsanaAddCommentAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseRequest(asanaAddCommentContract, request, {})
if (!parsed.success) return parsed.response
const { accessToken, taskGid, text } = parsed.data.body
const taskGidValidation = validateAlphanumericId(taskGid, 'taskGid', 100)
if (!taskGidValidation.isValid) {
return NextResponse.json({ error: taskGidValidation.error }, { status: 400 })
}
const url = `https://app.asana.com/api/1.0/tasks/${taskGid}/stories`
const body = {
data: {
text,
},
}
const response = await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
if (!response.ok) {
const errorText = await response.text()
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
try {
const errorData = JSON.parse(errorText)
const asanaError = errorData.errors?.[0]
if (asanaError) {
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
}
logger.error('Asana API error:', {
status: response.status,
statusText: response.statusText,
error: errorData,
})
} catch (_e) {
logger.error('Asana API error (unparsed):', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
}
return NextResponse.json(
{
success: false,
error: errorMessage,
details: errorText,
},
{ status: response.status }
)
}
const result = await response.json()
const story = result.data
return NextResponse.json({
success: true,
ts: new Date().toISOString(),
gid: story.gid,
text: story.text || '',
created_at: story.created_at,
created_by: story.created_by
? {
gid: story.created_by.gid,
name: story.created_by.name,
}
: undefined,
})
} catch (error) {
logger.error('Error processing request:', error)
return NextResponse.json(
{
error: 'Failed to add comment to Asana task',
details: (error as Error).message,
},
{ status: 500 }
)
}
})
@@ -0,0 +1,103 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { asanaAddFollowersContract } from '@/lib/api/contracts/tools/asana'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
export const dynamic = 'force-dynamic'
const logger = createLogger('AsanaAddFollowersAPI')
interface AsanaFollower {
gid: string
name: string
}
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseRequest(asanaAddFollowersContract, request, {})
if (!parsed.success) return parsed.response
const { accessToken, taskGid, followers } = parsed.data.body
const taskGidValidation = validateAlphanumericId(taskGid, 'taskGid', 100)
if (!taskGidValidation.isValid) {
return NextResponse.json({ error: taskGidValidation.error }, { status: 400 })
}
for (const follower of followers) {
const followerValidation = validateAlphanumericId(follower, 'follower', 100)
if (!followerValidation.isValid) {
return NextResponse.json({ error: followerValidation.error }, { status: 400 })
}
}
const url = `https://app.asana.com/api/1.0/tasks/${taskGid}/addFollowers?opt_fields=name,followers.name`
const response = await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ data: { followers } }),
})
if (!response.ok) {
const errorText = await response.text()
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
try {
const errorData = JSON.parse(errorText)
const asanaError = errorData.errors?.[0]
if (asanaError) {
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
}
logger.error('Asana API error:', {
status: response.status,
statusText: response.statusText,
error: errorData,
})
} catch (_e) {
logger.error('Asana API error (unparsed):', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
}
return NextResponse.json(
{ success: false, error: errorMessage, details: errorText },
{ status: response.status }
)
}
const result = await response.json()
const task = result.data
const taskFollowers: AsanaFollower[] = Array.isArray(task.followers) ? task.followers : []
return NextResponse.json({
success: true,
ts: new Date().toISOString(),
gid: task.gid,
name: task.name || '',
followers: taskFollowers.map((follower) => ({
gid: follower.gid,
name: follower.name,
})),
})
} catch (error) {
logger.error('Error adding followers to Asana task:', error)
return NextResponse.json(
{ error: 'Failed to add followers to Asana task', details: (error as Error).message },
{ status: 500 }
)
}
})
@@ -0,0 +1,103 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { asanaCreateProjectContract } from '@/lib/api/contracts/tools/asana'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
export const dynamic = 'force-dynamic'
const logger = createLogger('AsanaCreateProjectAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseRequest(asanaCreateProjectContract, request, {})
if (!parsed.success) return parsed.response
const { accessToken, workspace, name, notes } = parsed.data.body
const workspaceValidation = validateAlphanumericId(workspace, 'workspace', 100)
if (!workspaceValidation.isValid) {
return NextResponse.json({ error: workspaceValidation.error }, { status: 400 })
}
const projectData: Record<string, unknown> = { name, workspace }
if (notes) {
projectData.notes = notes
}
const response = await fetch(
'https://app.asana.com/api/1.0/projects?opt_fields=name,notes,archived,color,created_at,modified_at,permalink_url',
{
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ data: projectData }),
}
)
if (!response.ok) {
const errorText = await response.text()
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
try {
const errorData = JSON.parse(errorText)
const asanaError = errorData.errors?.[0]
if (asanaError) {
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
}
logger.error('Asana API error:', {
status: response.status,
statusText: response.statusText,
error: errorData,
})
} catch (_e) {
logger.error('Asana API error (unparsed):', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
}
return NextResponse.json(
{ success: false, error: errorMessage, details: errorText },
{ status: response.status }
)
}
const result = await response.json()
const project = result.data
return NextResponse.json({
success: true,
ts: new Date().toISOString(),
gid: project.gid,
name: project.name,
notes: project.notes || '',
archived: project.archived ?? false,
color: project.color ?? null,
created_at: project.created_at,
modified_at: project.modified_at,
permalink_url: project.permalink_url,
})
} catch (error) {
logger.error('Error creating Asana project:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{ error: getErrorMessage(error, 'Internal server error'), success: false },
{ status: 500 }
)
}
})
@@ -0,0 +1,92 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { asanaCreateSectionContract } from '@/lib/api/contracts/tools/asana'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
export const dynamic = 'force-dynamic'
const logger = createLogger('AsanaCreateSectionAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseRequest(asanaCreateSectionContract, request, {})
if (!parsed.success) return parsed.response
const { accessToken, projectGid, name } = parsed.data.body
const projectGidValidation = validateAlphanumericId(projectGid, 'projectGid', 100)
if (!projectGidValidation.isValid) {
return NextResponse.json({ error: projectGidValidation.error }, { status: 400 })
}
const url = `https://app.asana.com/api/1.0/projects/${projectGid}/sections`
const response = await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ data: { name } }),
})
if (!response.ok) {
const errorText = await response.text()
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
try {
const errorData = JSON.parse(errorText)
const asanaError = errorData.errors?.[0]
if (asanaError) {
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
}
logger.error('Asana API error:', {
status: response.status,
statusText: response.statusText,
error: errorData,
})
} catch (_e) {
logger.error('Asana API error (unparsed):', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
}
return NextResponse.json(
{ success: false, error: errorMessage, details: errorText },
{ status: response.status }
)
}
const result = await response.json()
const section = result.data
return NextResponse.json({
success: true,
ts: new Date().toISOString(),
gid: section.gid,
name: section.name,
created_at: section.created_at,
})
} catch (error) {
logger.error('Error creating Asana section:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{ error: getErrorMessage(error, 'Internal server error'), success: false },
{ status: 500 }
)
}
})
@@ -0,0 +1,106 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { asanaCreateSubtaskContract } from '@/lib/api/contracts/tools/asana'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
export const dynamic = 'force-dynamic'
const logger = createLogger('AsanaCreateSubtaskAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseRequest(asanaCreateSubtaskContract, request, {})
if (!parsed.success) return parsed.response
const { accessToken, taskGid, name, notes, assignee, due_on } = parsed.data.body
const taskGidValidation = validateAlphanumericId(taskGid, 'taskGid', 100)
if (!taskGidValidation.isValid) {
return NextResponse.json({ error: taskGidValidation.error }, { status: 400 })
}
const subtaskData: Record<string, unknown> = { name }
if (notes) {
subtaskData.notes = notes
}
if (assignee) {
subtaskData.assignee = assignee
}
if (due_on) {
subtaskData.due_on = due_on
}
const url = `https://app.asana.com/api/1.0/tasks/${taskGid}/subtasks?opt_fields=name,notes,completed,created_at,permalink_url`
const response = await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ data: subtaskData }),
})
if (!response.ok) {
const errorText = await response.text()
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
try {
const errorData = JSON.parse(errorText)
const asanaError = errorData.errors?.[0]
if (asanaError) {
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
}
logger.error('Asana API error:', {
status: response.status,
statusText: response.statusText,
error: errorData,
})
} catch (_e) {
logger.error('Asana API error (unparsed):', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
}
return NextResponse.json(
{ success: false, error: errorMessage, details: errorText },
{ status: response.status }
)
}
const result = await response.json()
const task = result.data
return NextResponse.json({
success: true,
ts: new Date().toISOString(),
gid: task.gid,
name: task.name,
notes: task.notes || '',
completed: task.completed || false,
created_at: task.created_at,
permalink_url: task.permalink_url,
})
} catch (error) {
logger.error('Error creating Asana subtask:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{ error: getErrorMessage(error, 'Internal server error'), success: false },
{ status: 500 }
)
}
})
@@ -0,0 +1,122 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { asanaCreateTaskContract } from '@/lib/api/contracts/tools/asana'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
export const dynamic = 'force-dynamic'
const logger = createLogger('AsanaCreateTaskAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseRequest(asanaCreateTaskContract, request, {})
if (!parsed.success) return parsed.response
const { accessToken, workspace, name, notes, assignee, due_on } = parsed.data.body
const workspaceValidation = validateAlphanumericId(workspace, 'workspace', 100)
if (!workspaceValidation.isValid) {
return NextResponse.json({ error: workspaceValidation.error }, { status: 400 })
}
const url =
'https://app.asana.com/api/1.0/tasks?opt_fields=name,notes,completed,created_at,permalink_url'
const taskData: Record<string, unknown> = {
name,
workspace,
}
if (notes) {
taskData.notes = notes
}
if (assignee) {
taskData.assignee = assignee
}
if (due_on) {
taskData.due_on = due_on
}
const body = { data: taskData }
const response = await fetch(url, {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
if (!response.ok) {
const errorText = await response.text()
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
try {
const errorData = JSON.parse(errorText)
const asanaError = errorData.errors?.[0]
if (asanaError) {
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
}
logger.error('Asana API error:', {
status: response.status,
statusText: response.statusText,
error: errorData,
})
} catch (_e) {
logger.error('Asana API error (unparsed):', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
}
return NextResponse.json(
{
success: false,
error: errorMessage,
details: errorText,
},
{ status: response.status }
)
}
const result = await response.json()
const task = result.data
return NextResponse.json({
success: true,
ts: new Date().toISOString(),
gid: task.gid,
name: task.name,
notes: task.notes || '',
completed: task.completed || false,
created_at: task.created_at,
permalink_url: task.permalink_url,
})
} catch (error) {
logger.error('Error creating Asana task:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: getErrorMessage(error, 'Internal server error'),
success: false,
},
{ status: 500 }
)
}
})
@@ -0,0 +1,81 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { asanaDeleteTaskContract } from '@/lib/api/contracts/tools/asana'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
export const dynamic = 'force-dynamic'
const logger = createLogger('AsanaDeleteTaskAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseRequest(asanaDeleteTaskContract, request, {})
if (!parsed.success) return parsed.response
const { accessToken, taskGid } = parsed.data.body
const taskGidValidation = validateAlphanumericId(taskGid, 'taskGid', 100)
if (!taskGidValidation.isValid) {
return NextResponse.json({ error: taskGidValidation.error }, { status: 400 })
}
const url = `https://app.asana.com/api/1.0/tasks/${taskGid}`
const response = await fetch(url, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
})
if (!response.ok) {
const errorText = await response.text()
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
try {
const errorData = JSON.parse(errorText)
const asanaError = errorData.errors?.[0]
if (asanaError) {
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
}
logger.error('Asana API error:', {
status: response.status,
statusText: response.statusText,
error: errorData,
})
} catch (_e) {
logger.error('Asana API error (unparsed):', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
}
return NextResponse.json(
{ success: false, error: errorMessage, details: errorText },
{ status: response.status }
)
}
return NextResponse.json({
success: true,
ts: new Date().toISOString(),
gid: taskGid,
deleted: true,
})
} catch (error) {
logger.error('Error deleting Asana task:', error)
return NextResponse.json(
{ error: 'Failed to delete Asana task', details: (error as Error).message },
{ status: 500 }
)
}
})
@@ -0,0 +1,92 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { asanaGetProjectContract } from '@/lib/api/contracts/tools/asana'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
export const dynamic = 'force-dynamic'
const logger = createLogger('AsanaGetProjectAPI')
const PROJECT_OPT_FIELDS = 'name,notes,archived,color,created_at,modified_at,permalink_url'
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseRequest(asanaGetProjectContract, request, {})
if (!parsed.success) return parsed.response
const { accessToken, projectGid } = parsed.data.body
const projectGidValidation = validateAlphanumericId(projectGid, 'projectGid', 100)
if (!projectGidValidation.isValid) {
return NextResponse.json({ error: projectGidValidation.error }, { status: 400 })
}
const url = `https://app.asana.com/api/1.0/projects/${projectGid}?opt_fields=${PROJECT_OPT_FIELDS}`
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
})
if (!response.ok) {
const errorText = await response.text()
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
try {
const errorData = JSON.parse(errorText)
const asanaError = errorData.errors?.[0]
if (asanaError) {
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
}
logger.error('Asana API error:', {
status: response.status,
statusText: response.statusText,
error: errorData,
})
} catch (_e) {
logger.error('Asana API error (unparsed):', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
}
return NextResponse.json(
{ success: false, error: errorMessage, details: errorText },
{ status: response.status }
)
}
const result = await response.json()
const project = result.data
return NextResponse.json({
success: true,
ts: new Date().toISOString(),
gid: project.gid,
name: project.name,
notes: project.notes || '',
archived: project.archived ?? false,
color: project.color ?? null,
created_at: project.created_at,
modified_at: project.modified_at,
permalink_url: project.permalink_url,
})
} catch (error) {
logger.error('Error processing request:', error)
return NextResponse.json(
{ error: 'Failed to retrieve Asana project', details: (error as Error).message },
{ status: 500 }
)
}
})
@@ -0,0 +1,94 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { asanaGetProjectsContract } from '@/lib/api/contracts/tools/asana'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
export const dynamic = 'force-dynamic'
const logger = createLogger('AsanaGetProjectsAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseRequest(asanaGetProjectsContract, request, {})
if (!parsed.success) return parsed.response
const { accessToken, workspace } = parsed.data.body
const workspaceValidation = validateAlphanumericId(workspace, 'workspace', 100)
if (!workspaceValidation.isValid) {
return NextResponse.json({ error: workspaceValidation.error }, { status: 400 })
}
const url = `https://app.asana.com/api/1.0/projects?workspace=${workspace}`
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
})
if (!response.ok) {
const errorText = await response.text()
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
try {
const errorData = JSON.parse(errorText)
const asanaError = errorData.errors?.[0]
if (asanaError) {
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
}
logger.error('Asana API error:', {
status: response.status,
statusText: response.statusText,
error: errorData,
})
} catch (_e) {
logger.error('Asana API error (unparsed):', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
}
return NextResponse.json(
{
success: false,
error: errorMessage,
details: errorText,
},
{ status: response.status }
)
}
const result = await response.json()
const projects = result.data
return NextResponse.json({
success: true,
ts: new Date().toISOString(),
projects: projects.map((project: { gid: string; name: string; resource_type: string }) => ({
gid: project.gid,
name: project.name,
resource_type: project.resource_type,
})),
})
} catch (error) {
logger.error('Error processing request:', error)
return NextResponse.json(
{
error: 'Failed to retrieve Asana projects',
details: (error as Error).message,
},
{ status: 500 }
)
}
})
@@ -0,0 +1,224 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { asanaGetTaskContract } from '@/lib/api/contracts/tools/asana'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
export const dynamic = 'force-dynamic'
const logger = createLogger('AsanaGetTaskAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseRequest(asanaGetTaskContract, request, {})
if (!parsed.success) return parsed.response
const { accessToken, taskGid, workspace, project, limit } = parsed.data.body
if (taskGid) {
const taskGidValidation = validateAlphanumericId(taskGid, 'taskGid', 100)
if (!taskGidValidation.isValid) {
return NextResponse.json({ error: taskGidValidation.error }, { status: 400 })
}
const url = `https://app.asana.com/api/1.0/tasks/${taskGid}?opt_fields=gid,name,notes,completed,assignee,assignee.name,due_on,created_at,modified_at,created_by,created_by.name,resource_type,resource_subtype`
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
})
if (!response.ok) {
const errorText = await response.text()
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
try {
const errorData = JSON.parse(errorText)
const asanaError = errorData.errors?.[0]
if (asanaError) {
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
}
logger.error('Asana API error:', {
status: response.status,
statusText: response.statusText,
error: errorData,
})
} catch (_e) {
logger.error('Asana API error (unparsed):', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
}
return NextResponse.json(
{
success: false,
error: errorMessage,
details: errorText,
},
{ status: response.status }
)
}
const result = await response.json()
const task = result.data
return NextResponse.json({
success: true,
ts: new Date().toISOString(),
gid: task.gid,
resource_type: task.resource_type,
resource_subtype: task.resource_subtype,
name: task.name,
notes: task.notes || '',
completed: task.completed || false,
assignee: task.assignee
? {
gid: task.assignee.gid,
name: task.assignee.name,
}
: undefined,
created_by: task.created_by
? {
gid: task.created_by.gid,
resource_type: task.created_by.resource_type,
name: task.created_by.name,
}
: undefined,
due_on: task.due_on || undefined,
created_at: task.created_at,
modified_at: task.modified_at,
})
}
if (!workspace && !project) {
logger.error('Either taskGid or workspace/project must be provided')
return NextResponse.json(
{ error: 'Either taskGid or workspace/project must be provided' },
{ status: 400 }
)
}
const params = new URLSearchParams()
if (project) {
const projectValidation = validateAlphanumericId(project, 'project', 100)
if (!projectValidation.isValid) {
return NextResponse.json({ error: projectValidation.error }, { status: 400 })
}
params.append('project', project)
} else if (workspace) {
const workspaceValidation = validateAlphanumericId(workspace, 'workspace', 100)
if (!workspaceValidation.isValid) {
return NextResponse.json({ error: workspaceValidation.error }, { status: 400 })
}
params.append('workspace', workspace)
}
if (limit) {
params.append('limit', String(limit))
} else {
params.append('limit', '50')
}
params.append(
'opt_fields',
'gid,name,notes,completed,assignee,assignee.name,due_on,created_at,modified_at,created_by,created_by.name,resource_type,resource_subtype'
)
const url = `https://app.asana.com/api/1.0/tasks?${params.toString()}`
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
})
if (!response.ok) {
const errorText = await response.text()
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
try {
const errorData = JSON.parse(errorText)
const asanaError = errorData.errors?.[0]
if (asanaError) {
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
}
logger.error('Asana API error:', {
status: response.status,
statusText: response.statusText,
error: errorData,
})
} catch (_e) {
logger.error('Asana API error (unparsed):', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
}
return NextResponse.json(
{
success: false,
error: errorMessage,
details: errorText,
},
{ status: response.status }
)
}
const result = await response.json()
const tasks = result.data
return NextResponse.json({
success: true,
ts: new Date().toISOString(),
tasks: tasks.map((task: any) => ({
gid: task.gid,
resource_type: task.resource_type,
resource_subtype: task.resource_subtype,
name: task.name,
notes: task.notes || '',
completed: task.completed || false,
assignee: task.assignee
? {
gid: task.assignee.gid,
name: task.assignee.name,
}
: undefined,
created_by: task.created_by
? {
gid: task.created_by.gid,
resource_type: task.created_by.resource_type,
name: task.created_by.name,
}
: undefined,
due_on: task.due_on || undefined,
created_at: task.created_at,
modified_at: task.modified_at,
})),
next_page: result.next_page,
})
} catch (error) {
logger.error('Error processing request:', error)
return NextResponse.json(
{
error: 'Failed to retrieve Asana task(s)',
details: (error as Error).message,
},
{ status: 500 }
)
}
})
@@ -0,0 +1,93 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { asanaListSectionsContract } from '@/lib/api/contracts/tools/asana'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
export const dynamic = 'force-dynamic'
const logger = createLogger('AsanaListSectionsAPI')
interface AsanaSection {
gid: string
name: string
resource_type?: string
}
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseRequest(asanaListSectionsContract, request, {})
if (!parsed.success) return parsed.response
const { accessToken, projectGid } = parsed.data.body
const projectGidValidation = validateAlphanumericId(projectGid, 'projectGid', 100)
if (!projectGidValidation.isValid) {
return NextResponse.json({ error: projectGidValidation.error }, { status: 400 })
}
const url = `https://app.asana.com/api/1.0/projects/${projectGid}/sections`
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
})
if (!response.ok) {
const errorText = await response.text()
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
try {
const errorData = JSON.parse(errorText)
const asanaError = errorData.errors?.[0]
if (asanaError) {
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
}
logger.error('Asana API error:', {
status: response.status,
statusText: response.statusText,
error: errorData,
})
} catch (_e) {
logger.error('Asana API error (unparsed):', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
}
return NextResponse.json(
{ success: false, error: errorMessage, details: errorText },
{ status: response.status }
)
}
const result = await response.json()
const sections: AsanaSection[] = Array.isArray(result.data) ? result.data : []
return NextResponse.json({
success: true,
ts: new Date().toISOString(),
sections: sections.map((section) => ({
gid: section.gid,
name: section.name,
resource_type: section.resource_type,
})),
})
} catch (error) {
logger.error('Error processing request:', error)
return NextResponse.json(
{ error: 'Failed to retrieve Asana sections', details: (error as Error).message },
{ status: 500 }
)
}
})
@@ -0,0 +1,87 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { asanaListWorkspacesContract } from '@/lib/api/contracts/tools/asana'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
export const dynamic = 'force-dynamic'
const logger = createLogger('AsanaListWorkspacesAPI')
interface AsanaWorkspace {
gid: string
name: string
resource_type?: string
}
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseRequest(asanaListWorkspacesContract, request, {})
if (!parsed.success) return parsed.response
const { accessToken } = parsed.data.body
const url = 'https://app.asana.com/api/1.0/workspaces?limit=100'
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
})
if (!response.ok) {
const errorText = await response.text()
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
try {
const errorData = JSON.parse(errorText)
const asanaError = errorData.errors?.[0]
if (asanaError) {
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
}
logger.error('Asana API error:', {
status: response.status,
statusText: response.statusText,
error: errorData,
})
} catch (_e) {
logger.error('Asana API error (unparsed):', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
}
return NextResponse.json(
{ success: false, error: errorMessage, details: errorText },
{ status: response.status }
)
}
const result = await response.json()
const workspaces: AsanaWorkspace[] = Array.isArray(result.data) ? result.data : []
return NextResponse.json({
success: true,
ts: new Date().toISOString(),
workspaces: workspaces.map((workspace) => ({
gid: workspace.gid,
name: workspace.name,
resource_type: workspace.resource_type,
})),
})
} catch (error) {
logger.error('Error processing request:', error)
return NextResponse.json(
{ error: 'Failed to retrieve Asana workspaces', details: (error as Error).message },
{ status: 500 }
)
}
})
@@ -0,0 +1,137 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { asanaSearchTasksContract } from '@/lib/api/contracts/tools/asana'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
export const dynamic = 'force-dynamic'
const logger = createLogger('AsanaSearchTasksAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseRequest(asanaSearchTasksContract, request, {})
if (!parsed.success) return parsed.response
const { accessToken, workspace, text, assignee, projects, completed } = parsed.data.body
const workspaceValidation = validateAlphanumericId(workspace, 'workspace', 100)
if (!workspaceValidation.isValid) {
return NextResponse.json({ error: workspaceValidation.error }, { status: 400 })
}
const params = new URLSearchParams()
if (text) {
params.append('text', text)
}
if (assignee) {
params.append('assignee.any', assignee)
}
if (projects && Array.isArray(projects) && projects.length > 0) {
params.append('projects.any', projects.join(','))
}
if (completed !== undefined) {
params.append('completed', String(completed))
}
params.append(
'opt_fields',
'gid,name,notes,completed,assignee,assignee.name,due_on,created_at,modified_at,created_by,created_by.name,resource_type,resource_subtype'
)
const url = `https://app.asana.com/api/1.0/workspaces/${workspace}/tasks/search?${params.toString()}`
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
})
if (!response.ok) {
const errorText = await response.text()
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
try {
const errorData = JSON.parse(errorText)
const asanaError = errorData.errors?.[0]
if (asanaError) {
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
}
logger.error('Asana API error:', {
status: response.status,
statusText: response.statusText,
error: errorData,
})
} catch (_e) {
logger.error('Asana API error (unparsed):', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
}
return NextResponse.json(
{
success: false,
error: errorMessage,
details: errorText,
},
{ status: response.status }
)
}
const result = await response.json()
const tasks = result.data
return NextResponse.json({
success: true,
ts: new Date().toISOString(),
tasks: tasks.map((task: any) => ({
gid: task.gid,
resource_type: task.resource_type,
resource_subtype: task.resource_subtype,
name: task.name,
notes: task.notes || '',
completed: task.completed || false,
assignee: task.assignee
? {
gid: task.assignee.gid,
name: task.assignee.name,
}
: undefined,
created_by: task.created_by
? {
gid: task.created_by.gid,
resource_type: task.created_by.resource_type,
name: task.created_by.name,
}
: undefined,
due_on: task.due_on || undefined,
created_at: task.created_at,
modified_at: task.modified_at,
})),
next_page: result.next_page,
})
} catch (error) {
logger.error('Error processing request:', error)
return NextResponse.json(
{
error: 'Failed to search Asana tasks',
details: (error as Error).message,
},
{ status: 500 }
)
}
})
@@ -0,0 +1,125 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { asanaUpdateTaskContract } from '@/lib/api/contracts/tools/asana'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
export const dynamic = 'force-dynamic'
const logger = createLogger('AsanaUpdateTaskAPI')
export const PUT = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseRequest(asanaUpdateTaskContract, request, {})
if (!parsed.success) return parsed.response
const { accessToken, taskGid, name, notes, assignee, completed, due_on } = parsed.data.body
const taskGidValidation = validateAlphanumericId(taskGid, 'taskGid', 100)
if (!taskGidValidation.isValid) {
return NextResponse.json({ error: taskGidValidation.error }, { status: 400 })
}
const url = `https://app.asana.com/api/1.0/tasks/${taskGid}`
const taskData: Record<string, unknown> = {}
if (name !== undefined) {
taskData.name = name
}
if (notes !== undefined) {
taskData.notes = notes
}
if (assignee !== undefined) {
taskData.assignee = assignee
}
if (completed !== undefined) {
taskData.completed = completed
}
if (due_on !== undefined) {
taskData.due_on = due_on
}
const body = { data: taskData }
const response = await fetch(url, {
method: 'PUT',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
if (!response.ok) {
const errorText = await response.text()
let errorMessage = `Asana API error: ${response.status} ${response.statusText}`
try {
const errorData = JSON.parse(errorText)
const asanaError = errorData.errors?.[0]
if (asanaError) {
errorMessage = `${asanaError.message || errorMessage} (${asanaError.help || ''})`
}
logger.error('Asana API error:', {
status: response.status,
statusText: response.statusText,
error: errorData,
})
} catch (_e) {
logger.error('Asana API error (unparsed):', {
status: response.status,
statusText: response.statusText,
error: errorText,
})
}
return NextResponse.json(
{
success: false,
error: errorMessage,
details: errorText,
},
{ status: response.status }
)
}
const result = await response.json()
const task = result.data
return NextResponse.json({
success: true,
ts: new Date().toISOString(),
gid: task.gid,
name: task.name,
notes: task.notes || '',
completed: task.completed || false,
modified_at: task.modified_at,
})
} catch (error) {
logger.error('Error updating Asana task:', {
error: toError(error).message,
stack: error instanceof Error ? error.stack : undefined,
})
return NextResponse.json(
{
error: getErrorMessage(error, 'Internal server error'),
success: false,
},
{ status: 500 }
)
}
})
@@ -0,0 +1,150 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { asanaWorkspacesSelectorContract } from '@/lib/api/contracts/selectors'
import { parseRequest } from '@/lib/api/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
const logger = createLogger('AsanaWorkspacesAPI')
export const dynamic = 'force-dynamic'
const ASANA_PAGE_LIMIT = 100
const ASANA_MAX_WORKSPACES_PAGES = 50
interface AsanaWorkspace {
gid: string
name: string
}
interface AsanaWorkspacesPage {
data?: AsanaWorkspace[]
next_page?: {
offset?: string
} | null
}
/**
* Lists all Asana workspaces using `limit`/`offset` pagination, following
* `next_page.offset` (an opaque token, passed back verbatim as `?offset=`)
* until `next_page` is null so the full set is returned. Bounded by
* `ASANA_MAX_WORKSPACES_PAGES`; logs a warning rather than silently dropping
* workspaces when the cap is hit.
*/
async function fetchAllWorkspaces(accessToken: string): Promise<AsanaWorkspace[]> {
const workspaces: AsanaWorkspace[] = []
let offset: string | undefined
for (let page = 0; page < ASANA_MAX_WORKSPACES_PAGES; page++) {
const url = new URL('https://app.asana.com/api/1.0/workspaces')
url.searchParams.set('limit', String(ASANA_PAGE_LIMIT))
if (offset) {
url.searchParams.set('offset', offset)
}
const response = await fetch(url.toString(), {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
throw new AsanaFetchError(response.status, errorData)
}
const data = (await response.json()) as AsanaWorkspacesPage
if (Array.isArray(data.data)) {
workspaces.push(...data.data)
}
offset = data.next_page?.offset || undefined
if (!offset) {
return workspaces
}
if (page === ASANA_MAX_WORKSPACES_PAGES - 1) {
logger.warn('Asana workspaces listing hit pagination cap; workspace list may be incomplete', {
pages: ASANA_MAX_WORKSPACES_PAGES,
})
}
}
return workspaces
}
class AsanaFetchError extends Error {
constructor(
readonly status: number,
readonly details: unknown
) {
super('Failed to fetch Asana workspaces')
this.name = 'AsanaFetchError'
}
}
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const parsed = await parseRequest(asanaWorkspacesSelectorContract, request, {})
if (!parsed.success) return parsed.response
const { credential, workflowId } = parsed.data.body
const authz = await authorizeCredentialUse(request, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{ error: 'Could not retrieve access token', authRequired: true },
{ status: 401 }
)
}
let allWorkspaces: AsanaWorkspace[]
try {
allWorkspaces = await fetchAllWorkspaces(accessToken)
} catch (error) {
if (error instanceof AsanaFetchError) {
logger.error('Failed to fetch Asana workspaces', {
status: error.status,
error: error.details,
})
return NextResponse.json(
{ error: 'Failed to fetch Asana workspaces', details: error.details },
{ status: error.status }
)
}
throw error
}
const workspaces = allWorkspaces.map((workspace) => ({
id: workspace.gid,
name: workspace.name,
}))
return NextResponse.json({ workspaces })
} catch (error) {
logger.error('Error processing Asana workspaces request:', error)
return NextResponse.json(
{ error: 'Failed to retrieve Asana workspaces', details: (error as Error).message },
{ status: 500 }
)
}
})
@@ -0,0 +1,78 @@
import { BatchGetQueryExecutionCommand } from '@aws-sdk/client-athena'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAthenaBatchGetQueryExecutionContract } from '@/lib/api/contracts/tools/aws/athena-batch-get-query-execution'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAthenaClient } from '@/app/api/tools/athena/utils'
const logger = createLogger('AthenaBatchGetQueryExecution')
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(awsAthenaBatchGetQueryExecutionContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const data = parsed.data.body
const client = createAthenaClient({
region: data.region,
accessKeyId: data.accessKeyId,
secretAccessKey: data.secretAccessKey,
})
try {
const command = new BatchGetQueryExecutionCommand({
QueryExecutionIds: data.queryExecutionIds,
})
const response = await client.send(command)
return NextResponse.json({
success: true,
output: {
queryExecutions: (response.QueryExecutions ?? []).map((execution) => ({
queryExecutionId: execution.QueryExecutionId ?? '',
query: execution.Query ?? null,
state: execution.Status?.State ?? null,
stateChangeReason: execution.Status?.StateChangeReason ?? null,
statementType: execution.StatementType ?? null,
database: execution.QueryExecutionContext?.Database ?? null,
catalog: execution.QueryExecutionContext?.Catalog ?? null,
workGroup: execution.WorkGroup ?? null,
submissionDateTime: execution.Status?.SubmissionDateTime?.getTime() ?? null,
completionDateTime: execution.Status?.CompletionDateTime?.getTime() ?? null,
dataScannedInBytes: execution.Statistics?.DataScannedInBytes ?? null,
engineExecutionTimeInMillis: execution.Statistics?.EngineExecutionTimeInMillis ?? null,
queryPlanningTimeInMillis: execution.Statistics?.QueryPlanningTimeInMillis ?? null,
queryQueueTimeInMillis: execution.Statistics?.QueryQueueTimeInMillis ?? null,
totalExecutionTimeInMillis: execution.Statistics?.TotalExecutionTimeInMillis ?? null,
outputLocation: execution.ResultConfiguration?.OutputLocation ?? null,
})),
unprocessedQueryExecutionIds: (response.UnprocessedQueryExecutionIds ?? []).map(
(item) => ({
queryExecutionId: item.QueryExecutionId ?? null,
errorCode: item.ErrorCode ?? null,
errorMessage: item.ErrorMessage ?? null,
})
),
},
})
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Failed to batch get Athena query executions')
logger.error('BatchGetQueryExecution failed', { error: errorMessage })
return NextResponse.json({ error: errorMessage }, { status: 500 })
}
})
@@ -0,0 +1,58 @@
import { CreateNamedQueryCommand } from '@aws-sdk/client-athena'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAthenaCreateNamedQueryContract } from '@/lib/api/contracts/tools/aws/athena-create-named-query'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAthenaClient } from '@/app/api/tools/athena/utils'
const logger = createLogger('AthenaCreateNamedQuery')
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(awsAthenaCreateNamedQueryContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const data = parsed.data.body
const client = createAthenaClient({
region: data.region,
accessKeyId: data.accessKeyId,
secretAccessKey: data.secretAccessKey,
})
const command = new CreateNamedQueryCommand({
Name: data.name,
Database: data.database,
QueryString: data.queryString,
...(data.description && { Description: data.description }),
...(data.workGroup && { WorkGroup: data.workGroup }),
})
const response = await client.send(command)
if (!response.NamedQueryId) {
throw new Error('No named query ID returned')
}
return NextResponse.json({
success: true,
output: {
namedQueryId: response.NamedQueryId,
},
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Failed to create Athena named query')
logger.error('CreateNamedQuery failed', { error: errorMessage })
return NextResponse.json({ error: errorMessage }, { status: 500 })
}
})
@@ -0,0 +1,54 @@
import { DeleteNamedQueryCommand } from '@aws-sdk/client-athena'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAthenaDeleteNamedQueryContract } from '@/lib/api/contracts/tools/aws/athena-delete-named-query'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAthenaClient } from '@/app/api/tools/athena/utils'
const logger = createLogger('AthenaDeleteNamedQuery')
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(awsAthenaDeleteNamedQueryContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const data = parsed.data.body
const client = createAthenaClient({
region: data.region,
accessKeyId: data.accessKeyId,
secretAccessKey: data.secretAccessKey,
})
try {
const command = new DeleteNamedQueryCommand({
NamedQueryId: data.namedQueryId,
})
await client.send(command)
return NextResponse.json({
success: true,
output: {
success: true,
},
})
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Failed to delete Athena named query')
logger.error('DeleteNamedQuery failed', { error: errorMessage })
return NextResponse.json({ error: errorMessage }, { status: 500 })
}
})
@@ -0,0 +1,60 @@
import { GetNamedQueryCommand } from '@aws-sdk/client-athena'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAthenaGetNamedQueryContract } from '@/lib/api/contracts/tools/aws/athena-get-named-query'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAthenaClient } from '@/app/api/tools/athena/utils'
const logger = createLogger('AthenaGetNamedQuery')
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(awsAthenaGetNamedQueryContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const data = parsed.data.body
const client = createAthenaClient({
region: data.region,
accessKeyId: data.accessKeyId,
secretAccessKey: data.secretAccessKey,
})
const command = new GetNamedQueryCommand({
NamedQueryId: data.namedQueryId,
})
const response = await client.send(command)
const namedQuery = response.NamedQuery
if (!namedQuery) {
throw new Error('No named query data returned')
}
return NextResponse.json({
success: true,
output: {
namedQueryId: namedQuery.NamedQueryId ?? data.namedQueryId,
name: namedQuery.Name ?? '',
description: namedQuery.Description ?? null,
database: namedQuery.Database ?? '',
queryString: namedQuery.QueryString ?? '',
workGroup: namedQuery.WorkGroup ?? null,
},
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Failed to get Athena named query')
logger.error('GetNamedQuery failed', { error: errorMessage })
return NextResponse.json({ error: errorMessage }, { status: 500 })
}
})
@@ -0,0 +1,70 @@
import { GetQueryExecutionCommand } from '@aws-sdk/client-athena'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAthenaGetQueryExecutionContract } from '@/lib/api/contracts/tools/aws/athena-get-query-execution'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAthenaClient } from '@/app/api/tools/athena/utils'
const logger = createLogger('AthenaGetQueryExecution')
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(awsAthenaGetQueryExecutionContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const data = parsed.data.body
const client = createAthenaClient({
region: data.region,
accessKeyId: data.accessKeyId,
secretAccessKey: data.secretAccessKey,
})
const command = new GetQueryExecutionCommand({
QueryExecutionId: data.queryExecutionId,
})
const response = await client.send(command)
const execution = response.QueryExecution
if (!execution) {
throw new Error('No query execution data returned')
}
return NextResponse.json({
success: true,
output: {
queryExecutionId: execution.QueryExecutionId ?? data.queryExecutionId,
query: execution.Query ?? '',
state: execution.Status?.State ?? 'UNKNOWN',
stateChangeReason: execution.Status?.StateChangeReason ?? null,
statementType: execution.StatementType ?? null,
database: execution.QueryExecutionContext?.Database ?? null,
catalog: execution.QueryExecutionContext?.Catalog ?? null,
workGroup: execution.WorkGroup ?? null,
submissionDateTime: execution.Status?.SubmissionDateTime?.getTime() ?? null,
completionDateTime: execution.Status?.CompletionDateTime?.getTime() ?? null,
dataScannedInBytes: execution.Statistics?.DataScannedInBytes ?? null,
engineExecutionTimeInMillis: execution.Statistics?.EngineExecutionTimeInMillis ?? null,
queryPlanningTimeInMillis: execution.Statistics?.QueryPlanningTimeInMillis ?? null,
queryQueueTimeInMillis: execution.Statistics?.QueryQueueTimeInMillis ?? null,
totalExecutionTimeInMillis: execution.Statistics?.TotalExecutionTimeInMillis ?? null,
outputLocation: execution.ResultConfiguration?.OutputLocation ?? null,
},
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Failed to get Athena query execution')
logger.error('GetQueryExecution failed', { error: errorMessage })
return NextResponse.json({ error: errorMessage }, { status: 500 })
}
})
@@ -0,0 +1,76 @@
import { GetQueryResultsCommand } from '@aws-sdk/client-athena'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAthenaGetQueryResultsContract } from '@/lib/api/contracts/tools/aws/athena-get-query-results'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAthenaClient } from '@/app/api/tools/athena/utils'
const logger = createLogger('AthenaGetQueryResults')
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(awsAthenaGetQueryResultsContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const data = parsed.data.body
const client = createAthenaClient({
region: data.region,
accessKeyId: data.accessKeyId,
secretAccessKey: data.secretAccessKey,
})
const isFirstPage = !data.nextToken
const adjustedMaxResults =
data.maxResults !== undefined && isFirstPage ? data.maxResults + 1 : data.maxResults
const command = new GetQueryResultsCommand({
QueryExecutionId: data.queryExecutionId,
...(adjustedMaxResults !== undefined && { MaxResults: adjustedMaxResults }),
...(data.nextToken && { NextToken: data.nextToken }),
})
const response = await client.send(command)
const columnInfo = response.ResultSet?.ResultSetMetadata?.ColumnInfo ?? []
const columns = columnInfo.map((col) => ({
name: col.Name ?? '',
type: col.Type ?? 'varchar',
}))
const rawRows = response.ResultSet?.Rows ?? []
const dataRows = data.nextToken ? rawRows : rawRows.slice(1)
const rows = dataRows.map((row) => {
const record: Record<string, string> = {}
const rowData = row.Data ?? []
for (let i = 0; i < columns.length; i++) {
record[columns[i].name] = rowData[i]?.VarCharValue ?? ''
}
return record
})
return NextResponse.json({
success: true,
output: {
columns,
rows,
nextToken: response.NextToken ?? null,
updateCount: response.UpdateCount ?? null,
},
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Failed to get Athena query results')
logger.error('GetQueryResults failed', { error: errorMessage })
return NextResponse.json({ error: errorMessage }, { status: 500 })
}
})
@@ -0,0 +1,61 @@
import { ListDatabasesCommand } from '@aws-sdk/client-athena'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAthenaListDatabasesContract } from '@/lib/api/contracts/tools/aws/athena-list-databases'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAthenaClient } from '@/app/api/tools/athena/utils'
const logger = createLogger('AthenaListDatabases')
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(awsAthenaListDatabasesContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const data = parsed.data.body
const client = createAthenaClient({
region: data.region,
accessKeyId: data.accessKeyId,
secretAccessKey: data.secretAccessKey,
})
try {
const command = new ListDatabasesCommand({
CatalogName: data.catalogName,
...(data.workGroup && { WorkGroup: data.workGroup }),
...(data.maxResults !== undefined && { MaxResults: data.maxResults }),
...(data.nextToken && { NextToken: data.nextToken }),
})
const response = await client.send(command)
return NextResponse.json({
success: true,
output: {
databases: (response.DatabaseList ?? []).map((db) => ({
name: db.Name ?? '',
description: db.Description ?? null,
})),
nextToken: response.NextToken ?? null,
},
})
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Failed to list Athena databases')
logger.error('ListDatabases failed', { error: errorMessage })
return NextResponse.json({ error: errorMessage }, { status: 500 })
}
})
@@ -0,0 +1,53 @@
import { ListNamedQueriesCommand } from '@aws-sdk/client-athena'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAthenaListNamedQueriesContract } from '@/lib/api/contracts/tools/aws/athena-list-named-queries'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAthenaClient } from '@/app/api/tools/athena/utils'
const logger = createLogger('AthenaListNamedQueries')
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(awsAthenaListNamedQueriesContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const data = parsed.data.body
const client = createAthenaClient({
region: data.region,
accessKeyId: data.accessKeyId,
secretAccessKey: data.secretAccessKey,
})
const command = new ListNamedQueriesCommand({
...(data.workGroup && { WorkGroup: data.workGroup }),
...(data.maxResults !== undefined && { MaxResults: data.maxResults }),
...(data.nextToken && { NextToken: data.nextToken }),
})
const response = await client.send(command)
return NextResponse.json({
success: true,
output: {
namedQueryIds: response.NamedQueryIds ?? [],
nextToken: response.NextToken ?? null,
},
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Failed to list Athena named queries')
logger.error('ListNamedQueries failed', { error: errorMessage })
return NextResponse.json({ error: errorMessage }, { status: 500 })
}
})
@@ -0,0 +1,53 @@
import { ListQueryExecutionsCommand } from '@aws-sdk/client-athena'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAthenaListQueryExecutionsContract } from '@/lib/api/contracts/tools/aws/athena-list-query-executions'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAthenaClient } from '@/app/api/tools/athena/utils'
const logger = createLogger('AthenaListQueryExecutions')
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(awsAthenaListQueryExecutionsContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const data = parsed.data.body
const client = createAthenaClient({
region: data.region,
accessKeyId: data.accessKeyId,
secretAccessKey: data.secretAccessKey,
})
const command = new ListQueryExecutionsCommand({
...(data.workGroup && { WorkGroup: data.workGroup }),
...(data.maxResults !== undefined && { MaxResults: data.maxResults }),
...(data.nextToken && { NextToken: data.nextToken }),
})
const response = await client.send(command)
return NextResponse.json({
success: true,
output: {
queryExecutionIds: response.QueryExecutionIds ?? [],
nextToken: response.NextToken ?? null,
},
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Failed to list Athena query executions')
logger.error('ListQueryExecutions failed', { error: errorMessage })
return NextResponse.json({ error: errorMessage }, { status: 500 })
}
})
@@ -0,0 +1,75 @@
import { ListTableMetadataCommand } from '@aws-sdk/client-athena'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAthenaListTableMetadataContract } from '@/lib/api/contracts/tools/aws/athena-list-table-metadata'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAthenaClient } from '@/app/api/tools/athena/utils'
const logger = createLogger('AthenaListTableMetadata')
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(awsAthenaListTableMetadataContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const data = parsed.data.body
const client = createAthenaClient({
region: data.region,
accessKeyId: data.accessKeyId,
secretAccessKey: data.secretAccessKey,
})
try {
const command = new ListTableMetadataCommand({
CatalogName: data.catalogName,
DatabaseName: data.databaseName,
...(data.expression && { Expression: data.expression }),
...(data.workGroup && { WorkGroup: data.workGroup }),
...(data.maxResults !== undefined && { MaxResults: data.maxResults }),
...(data.nextToken && { NextToken: data.nextToken }),
})
const response = await client.send(command)
return NextResponse.json({
success: true,
output: {
tables: (response.TableMetadataList ?? []).map((table) => ({
name: table.Name ?? '',
tableType: table.TableType ?? null,
createTime: table.CreateTime?.getTime() ?? null,
lastAccessTime: table.LastAccessTime?.getTime() ?? null,
columns: (table.Columns ?? []).map((col) => ({
name: col.Name ?? '',
type: col.Type ?? null,
comment: col.Comment ?? null,
})),
partitionKeys: (table.PartitionKeys ?? []).map((col) => ({
name: col.Name ?? '',
type: col.Type ?? null,
comment: col.Comment ?? null,
})),
})),
nextToken: response.NextToken ?? null,
},
})
} finally {
client.destroy()
}
} catch (error) {
const errorMessage = getErrorMessage(error, 'Failed to list Athena table metadata')
logger.error('ListTableMetadata failed', { error: errorMessage })
return NextResponse.json({ error: errorMessage }, { status: 500 })
}
})
@@ -0,0 +1,70 @@
import { StartQueryExecutionCommand } from '@aws-sdk/client-athena'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAthenaStartQueryContract } from '@/lib/api/contracts/tools/aws/athena-start-query'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAthenaClient } from '@/app/api/tools/athena/utils'
const logger = createLogger('AthenaStartQuery')
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(awsAthenaStartQueryContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const data = parsed.data.body
const client = createAthenaClient({
region: data.region,
accessKeyId: data.accessKeyId,
secretAccessKey: data.secretAccessKey,
})
const command = new StartQueryExecutionCommand({
QueryString: data.queryString,
...(data.database || data.catalog
? {
QueryExecutionContext: {
...(data.database && { Database: data.database }),
...(data.catalog && { Catalog: data.catalog }),
},
}
: {}),
...(data.outputLocation
? {
ResultConfiguration: {
OutputLocation: data.outputLocation,
},
}
: {}),
...(data.workGroup && { WorkGroup: data.workGroup }),
})
const response = await client.send(command)
if (!response.QueryExecutionId) {
throw new Error('No query execution ID returned')
}
return NextResponse.json({
success: true,
output: {
queryExecutionId: response.QueryExecutionId,
},
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Failed to start Athena query')
logger.error('StartQuery failed', { error: errorMessage })
return NextResponse.json({ error: errorMessage }, { status: 500 })
}
})
@@ -0,0 +1,50 @@
import { StopQueryExecutionCommand } from '@aws-sdk/client-athena'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { awsAthenaStopQueryContract } from '@/lib/api/contracts/tools/aws/athena-stop-query'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { createAthenaClient } from '@/app/api/tools/athena/utils'
const logger = createLogger('AthenaStopQuery')
export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(awsAthenaStopQueryContract, request, {
errorFormat: 'details',
logger,
})
if (!parsed.success) return parsed.response
const data = parsed.data.body
const client = createAthenaClient({
region: data.region,
accessKeyId: data.accessKeyId,
secretAccessKey: data.secretAccessKey,
})
const command = new StopQueryExecutionCommand({
QueryExecutionId: data.queryExecutionId,
})
await client.send(command)
return NextResponse.json({
success: true,
output: {
success: true,
},
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Failed to stop Athena query')
logger.error('StopQuery failed', { error: errorMessage })
return NextResponse.json({ error: errorMessage }, { status: 500 })
}
})
+17
View File
@@ -0,0 +1,17 @@
import { AthenaClient } from '@aws-sdk/client-athena'
interface AwsCredentials {
region: string
accessKeyId: string
secretAccessKey: string
}
export function createAthenaClient(config: AwsCredentials): AthenaClient {
return new AthenaClient({
region: config.region,
credentials: {
accessKeyId: config.accessKeyId,
secretAccessKey: config.secretAccessKey,
},
})
}
@@ -0,0 +1,92 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { attioListsSelectorContract } from '@/lib/api/contracts/selectors/attio'
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
const logger = createLogger('AttioListsAPI')
export const dynamic = 'force-dynamic'
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const parsed = await parseRequest(
attioListsSelectorContract,
request,
{},
{
validationErrorResponse: (error) => {
logger.error('Missing credential in request')
return NextResponse.json(
{ error: getValidationErrorMessage(error, 'Invalid request') },
{ status: 400 }
)
},
}
)
if (!parsed.success) return parsed.response
const { credential, workflowId } = parsed.data.body
const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{ error: 'Could not retrieve access token', authRequired: true },
{ status: 401 }
)
}
const response = await fetch('https://api.attio.com/v2/lists', {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
logger.error('Failed to fetch Attio lists', {
status: response.status,
error: errorData,
})
return NextResponse.json(
{ error: 'Failed to fetch Attio lists', details: errorData },
{ status: response.status }
)
}
const data = await response.json()
const lists = (data.data || []).map((list: { api_slug: string; name: string }) => ({
id: list.api_slug,
name: list.name,
}))
return NextResponse.json({ lists })
} catch (error) {
logger.error('Error processing Attio lists request:', error)
return NextResponse.json(
{ error: 'Failed to retrieve Attio lists', details: (error as Error).message },
{ status: 500 }
)
}
})
@@ -0,0 +1,92 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { attioObjectsSelectorContract } from '@/lib/api/contracts/selectors/attio'
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
const logger = createLogger('AttioObjectsAPI')
export const dynamic = 'force-dynamic'
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const parsed = await parseRequest(
attioObjectsSelectorContract,
request,
{},
{
validationErrorResponse: (error) => {
logger.error('Missing credential in request')
return NextResponse.json(
{ error: getValidationErrorMessage(error, 'Invalid request') },
{ status: 400 }
)
},
}
)
if (!parsed.success) return parsed.response
const { credential, workflowId } = parsed.data.body
const authz = await authorizeCredentialUse(request as any, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{ error: 'Could not retrieve access token', authRequired: true },
{ status: 401 }
)
}
const response = await fetch('https://api.attio.com/v2/objects', {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
logger.error('Failed to fetch Attio objects', {
status: response.status,
error: errorData,
})
return NextResponse.json(
{ error: 'Failed to fetch Attio objects', details: errorData },
{ status: response.status }
)
}
const data = await response.json()
const objects = (data.data || []).map((obj: { api_slug: string; singular_noun: string }) => ({
id: obj.api_slug,
name: obj.singular_noun,
}))
return NextResponse.json({ objects })
} catch (error) {
logger.error('Error processing Attio objects request:', error)
return NextResponse.json(
{ error: 'Failed to retrieve Attio objects', details: (error as Error).message },
{ status: 500 }
)
}
})
+141
View File
@@ -0,0 +1,141 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { boxUploadContract } from '@/lib/api/contracts/storage-transfer'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils'
import { downloadServableFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { docNotReadyResponse } from '@/lib/uploads/utils/servable-file-response'
import { assertToolFileAccess } from '@/app/api/files/authorization'
export const dynamic = 'force-dynamic'
const logger = createLogger('BoxUploadAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Box upload attempt: ${authResult.error}`)
return NextResponse.json(
{ success: false, error: authResult.error || 'Authentication required' },
{ status: 401 }
)
}
logger.info(`[${requestId}] Authenticated Box upload request via ${authResult.authType}`)
const parsed = await parseRequest(boxUploadContract, request, {})
if (!parsed.success) return parsed.response
const validatedData = parsed.data.body
let fileBuffer: Buffer
let fileName: string
if (validatedData.file) {
const userFiles = processFilesToUserFiles(
[validatedData.file as RawFileInput],
requestId,
logger
)
if (userFiles.length === 0) {
return NextResponse.json({ success: false, error: 'Invalid file input' }, { status: 400 })
}
const userFile = userFiles[0]
logger.info(`[${requestId}] Downloading file: ${userFile.name} (${userFile.size} bytes)`)
const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger)
if (denied) return denied
try {
const result = await downloadServableFileFromStorage(userFile, requestId, logger)
fileBuffer = result.buffer
} catch (error) {
const notReady = docNotReadyResponse(error)
if (notReady) return notReady
return NextResponse.json(
{ success: false, error: getErrorMessage(error, 'Failed to download file') },
{ status: 500 }
)
}
fileName = validatedData.fileName || userFile.name
} else if (validatedData.fileContent) {
logger.info(`[${requestId}] Using legacy base64 content input`)
fileBuffer = Buffer.from(validatedData.fileContent, 'base64')
fileName = validatedData.fileName || 'file'
} else {
return NextResponse.json({ success: false, error: 'File is required' }, { status: 400 })
}
logger.info(
`[${requestId}] Uploading to Box folder ${validatedData.parentFolderId}: ${fileName} (${fileBuffer.length} bytes)`
)
const attributes = JSON.stringify({
name: fileName,
parent: { id: validatedData.parentFolderId },
})
const formData = new FormData()
formData.append('attributes', attributes)
formData.append(
'file',
new Blob([new Uint8Array(fileBuffer)], { type: 'application/octet-stream' }),
fileName
)
const response = await fetch('https://upload.box.com/api/2.0/files/content', {
method: 'POST',
headers: {
Authorization: `Bearer ${validatedData.accessToken}`,
},
body: formData,
})
const data = await response.json()
if (!response.ok) {
const errorMessage = data.message || 'Failed to upload file'
logger.error(`[${requestId}] Box API error:`, { status: response.status, data })
return NextResponse.json({ success: false, error: errorMessage }, { status: response.status })
}
const file = data.entries?.[0]
if (!file) {
return NextResponse.json(
{ success: false, error: 'No file returned in upload response' },
{ status: 500 }
)
}
logger.info(`[${requestId}] File uploaded successfully: ${file.name} (ID: ${file.id})`)
return NextResponse.json({
success: true,
output: {
id: file.id ?? '',
name: file.name ?? '',
size: file.size ?? 0,
sha1: file.sha1 ?? null,
createdAt: file.created_at ?? null,
modifiedAt: file.modified_at ?? null,
parentId: file.parent?.id ?? null,
parentName: file.parent?.name ?? null,
},
})
} catch (error) {
logger.error(`[${requestId}] Unexpected error:`, error)
return NextResponse.json(
{ success: false, error: getErrorMessage(error, 'Unknown error') },
{ status: 500 }
)
}
})
@@ -0,0 +1,251 @@
/**
* @vitest-environment node
*/
import {
createMockRequest,
hybridAuthMockFns,
inputValidationMock,
inputValidationMockFns,
} from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { mockProcessFilesToUserFiles, mockDownloadFileFromStorage, mockAssertToolFileAccess } =
vi.hoisted(() => ({
mockProcessFilesToUserFiles: vi.fn(),
mockDownloadFileFromStorage: vi.fn(),
mockAssertToolFileAccess: vi.fn(),
}))
vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock)
vi.mock('@/lib/uploads/utils/file-utils', () => ({
processFilesToUserFiles: mockProcessFilesToUserFiles,
}))
vi.mock('@/lib/uploads/utils/file-utils.server', () => ({
downloadServableFileFromStorage: mockDownloadFileFromStorage,
}))
vi.mock('@/app/api/files/authorization', () => ({
assertToolFileAccess: mockAssertToolFileAccess,
}))
import { POST } from '@/app/api/tools/brex/upload-receipt/route'
const mockFetch = vi.fn()
const PINNED_IP = '52.216.0.1'
const baseBody = {
apiKey: 'bxt_test_token',
expenseId: 'expense_123',
file: { key: 'uploads/receipt.pdf', name: 'receipt.pdf', size: 5, type: 'application/pdf' },
}
function jsonResponse(body: unknown, status = 200) {
return {
ok: status >= 200 && status < 300,
status,
text: async () => JSON.stringify(body),
json: async () => body,
}
}
beforeEach(() => {
vi.clearAllMocks()
vi.stubGlobal('fetch', mockFetch)
hybridAuthMockFns.mockCheckInternalAuth.mockResolvedValue({
success: true,
userId: 'user-1',
authType: 'internal_jwt',
})
inputValidationMockFns.mockValidateUrlWithDNS.mockResolvedValue({
isValid: true,
resolvedIP: PINNED_IP,
})
inputValidationMockFns.mockSecureFetchWithPinnedIP.mockResolvedValue(jsonResponse({}))
mockProcessFilesToUserFiles.mockReturnValue([
{ key: 'uploads/receipt.pdf', name: 'receipt.pdf', size: 5, type: 'application/pdf' },
])
mockAssertToolFileAccess.mockResolvedValue(null)
mockDownloadFileFromStorage.mockResolvedValue({
buffer: Buffer.from('receipt-bytes'),
contentType: 'application/pdf',
})
})
describe('POST /api/tools/brex/upload-receipt', () => {
it('rejects unauthenticated requests', async () => {
hybridAuthMockFns.mockCheckInternalAuth.mockResolvedValueOnce({
success: false,
error: 'unauthorized',
})
const response = await POST(createMockRequest('POST', baseBody))
expect(response.status).toBe(401)
expect(mockFetch).not.toHaveBeenCalled()
})
it('creates a receipt upload for an expense and PUTs the file to the pre-signed URL', async () => {
mockFetch.mockResolvedValueOnce(
jsonResponse({ id: 'receipt_1', uri: 'https://s3.example.com/presigned' })
)
const response = await POST(createMockRequest('POST', baseBody))
expect(response.status).toBe(200)
const data = await response.json()
expect(data).toEqual({
success: true,
output: { receiptId: 'receipt_1', receiptName: 'receipt.pdf', expenseId: 'expense_123' },
})
expect(mockFetch).toHaveBeenCalledTimes(1)
const [createUrl, createInit] = mockFetch.mock.calls[0]
expect(createUrl).toBe('https://api.brex.com/v1/expenses/card/expense_123/receipt_upload')
expect(createInit.method).toBe('POST')
expect(createInit.headers.Authorization).toBe('Bearer bxt_test_token')
expect(JSON.parse(createInit.body)).toEqual({ receipt_name: 'receipt.pdf' })
expect(inputValidationMockFns.mockValidateUrlWithDNS).toHaveBeenCalledWith(
'https://s3.example.com/presigned',
'uri'
)
const [uploadUrl, pinnedIP, uploadInit] =
inputValidationMockFns.mockSecureFetchWithPinnedIP.mock.calls[0]
expect(uploadUrl).toBe('https://s3.example.com/presigned')
expect(pinnedIP).toBe(PINNED_IP)
expect(uploadInit.method).toBe('PUT')
})
it('rejects a whitespace-only expense ID instead of falling back to receipt match', async () => {
const response = await POST(createMockRequest('POST', { ...baseBody, expenseId: ' ' }))
expect(response.status).toBe(400)
expect(mockFetch).not.toHaveBeenCalled()
})
it('trims a padded expense ID before building the upload URL', async () => {
mockFetch.mockResolvedValueOnce(
jsonResponse({ id: 'receipt_5', uri: 'https://s3.example.com/presigned' })
)
const response = await POST(
createMockRequest('POST', { ...baseBody, expenseId: ' expense_123 ' })
)
expect(response.status).toBe(200)
const [createUrl] = mockFetch.mock.calls[0]
expect(createUrl).toBe('https://api.brex.com/v1/expenses/card/expense_123/receipt_upload')
const data = await response.json()
expect(data.output.expenseId).toBe('expense_123')
})
it('rejects a whitespace-only receipt name', async () => {
const response = await POST(createMockRequest('POST', { ...baseBody, receiptName: ' ' }))
expect(response.status).toBe(400)
expect(mockFetch).not.toHaveBeenCalled()
})
it('rejects an API key containing header-breaking characters', async () => {
const response = await POST(
createMockRequest('POST', { ...baseBody, apiKey: 'bxt_test\r\nX-Injected: 1' })
)
expect(response.status).toBe(400)
expect(mockFetch).not.toHaveBeenCalled()
})
it('uses receipt match when no expense ID is provided', async () => {
mockFetch.mockResolvedValueOnce(
jsonResponse({ id: 'receipt_2', uri: 'https://s3.example.com/presigned' })
)
const response = await POST(
createMockRequest('POST', { apiKey: 'bxt_test_token', file: baseBody.file })
)
expect(response.status).toBe(200)
const data = await response.json()
expect(data.output).toEqual({
receiptId: 'receipt_2',
receiptName: 'receipt.pdf',
expenseId: null,
})
const [createUrl] = mockFetch.mock.calls[0]
expect(createUrl).toBe('https://api.brex.com/v1/expenses/card/receipt_match')
})
it('honors a receipt name override', async () => {
mockFetch.mockResolvedValueOnce(
jsonResponse({ id: 'receipt_3', uri: 'https://s3.example.com/presigned' })
)
const response = await POST(
createMockRequest('POST', { ...baseBody, receiptName: 'march-dinner.pdf' })
)
expect(response.status).toBe(200)
const [, createInit] = mockFetch.mock.calls[0]
expect(JSON.parse(createInit.body)).toEqual({ receipt_name: 'march-dinner.pdf' })
})
it('propagates Brex API errors', async () => {
mockFetch.mockResolvedValueOnce(jsonResponse({ message: 'Expense not found' }, 404))
const response = await POST(createMockRequest('POST', baseBody))
expect(response.status).toBe(404)
const data = await response.json()
expect(data.success).toBe(false)
expect(data.error).toContain('Expense not found')
expect(mockFetch).toHaveBeenCalledTimes(1)
expect(inputValidationMockFns.mockSecureFetchWithPinnedIP).not.toHaveBeenCalled()
})
it('rejects files over the 50 MB limit', async () => {
mockDownloadFileFromStorage.mockResolvedValueOnce({
buffer: Buffer.alloc(50 * 1024 * 1024 + 1),
contentType: 'application/pdf',
})
const response = await POST(createMockRequest('POST', baseBody))
expect(response.status).toBe(400)
const data = await response.json()
expect(data.error).toContain('50 MB')
expect(mockFetch).not.toHaveBeenCalled()
})
it('blocks pre-signed URLs that fail SSRF validation', async () => {
mockFetch.mockResolvedValueOnce(
jsonResponse({ id: 'receipt_6', uri: 'https://169.254.169.254/latest/meta-data' })
)
inputValidationMockFns.mockValidateUrlWithDNS.mockResolvedValueOnce({
isValid: false,
error: 'uri resolves to a blocked IP address',
})
const response = await POST(createMockRequest('POST', baseBody))
expect(response.status).toBe(502)
const data = await response.json()
expect(data.error).toContain('invalid upload URL')
expect(inputValidationMockFns.mockSecureFetchWithPinnedIP).not.toHaveBeenCalled()
})
it('fails when the pre-signed upload fails', async () => {
mockFetch.mockResolvedValueOnce(
jsonResponse({ id: 'receipt_4', uri: 'https://s3.example.com/presigned' })
)
inputValidationMockFns.mockSecureFetchWithPinnedIP.mockResolvedValueOnce(jsonResponse({}, 403))
const response = await POST(createMockRequest('POST', baseBody))
expect(response.status).toBe(502)
const data = await response.json()
expect(data.success).toBe(false)
})
it('denies access to files the caller cannot read', async () => {
const deniedResponse = new Response(
JSON.stringify({ success: false, error: 'File not found' }),
{
status: 404,
}
)
mockAssertToolFileAccess.mockResolvedValueOnce(deniedResponse)
const response = await POST(createMockRequest('POST', baseBody))
expect(response.status).toBe(404)
expect(mockFetch).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,160 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { brexUploadReceiptContract } from '@/lib/api/contracts/tools/brex'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
secureFetchWithPinnedIP,
validateUrlWithDNS,
} from '@/lib/core/security/input-validation.server'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils'
import { downloadServableFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { docNotReadyResponse } from '@/lib/uploads/utils/servable-file-response'
import { assertToolFileAccess } from '@/app/api/files/authorization'
import { BREX_API_BASE, buildBrexHeaders } from '@/tools/brex/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('BrexUploadReceiptAPI')
const MAX_RECEIPT_SIZE_BYTES = 50 * 1024 * 1024
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Brex receipt upload attempt: ${authResult.error}`)
return NextResponse.json(
{ success: false, error: authResult.error || 'Authentication required' },
{ status: 401 }
)
}
const parsed = await parseRequest(brexUploadReceiptContract, request, {})
if (!parsed.success) return parsed.response
const { apiKey, expenseId, file, receiptName } = parsed.data.body
const userFiles = processFilesToUserFiles([file as RawFileInput], requestId, logger)
if (userFiles.length === 0) {
return NextResponse.json({ success: false, error: 'Invalid file input' }, { status: 400 })
}
const userFile = userFiles[0]
const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger)
if (denied) return denied
let fileBuffer: Buffer
try {
const resolved = await downloadServableFileFromStorage(userFile, requestId, logger)
fileBuffer = resolved.buffer
} catch (error) {
const notReady = docNotReadyResponse(error)
if (notReady) return notReady
logger.error(`[${requestId}] Failed to download receipt file:`, error)
return NextResponse.json(
{ success: false, error: getErrorMessage(error, 'Unknown error') },
{ status: 500 }
)
}
if (fileBuffer.length > MAX_RECEIPT_SIZE_BYTES) {
return NextResponse.json(
{ success: false, error: 'Receipt file exceeds the 50 MB limit' },
{ status: 400 }
)
}
const effectiveReceiptName = receiptName || userFile.name
const endpoint = expenseId
? `${BREX_API_BASE}/v1/expenses/card/${encodeURIComponent(expenseId)}/receipt_upload`
: `${BREX_API_BASE}/v1/expenses/card/receipt_match`
logger.info(
`[${requestId}] Creating Brex ${expenseId ? 'receipt upload' : 'receipt match'}: ${effectiveReceiptName} (${fileBuffer.length} bytes)`
)
const createResponse = await fetch(endpoint, {
method: 'POST',
headers: buildBrexHeaders(apiKey),
body: JSON.stringify({ receipt_name: effectiveReceiptName }),
})
if (!createResponse.ok) {
const errorText = await createResponse.text()
logger.error(`[${requestId}] Brex API error:`, {
status: createResponse.status,
error: errorText,
})
let message = errorText
try {
message = JSON.parse(errorText).message ?? errorText
} catch {
message = errorText
}
return NextResponse.json(
{ success: false, error: `Brex API error (${createResponse.status}): ${message}` },
{ status: createResponse.status }
)
}
const createData = await createResponse.json()
if (!createData.uri || !createData.id) {
return NextResponse.json(
{ success: false, error: 'Brex did not return an upload URL' },
{ status: 502 }
)
}
const uriValidation = await validateUrlWithDNS(createData.uri, 'uri')
if (!uriValidation.isValid) {
logger.error(`[${requestId}] Pre-signed upload URL failed SSRF validation:`, {
error: uriValidation.error,
})
return NextResponse.json(
{ success: false, error: 'Brex returned an invalid upload URL' },
{ status: 502 }
)
}
const uploadResponse = await secureFetchWithPinnedIP(
createData.uri,
uriValidation.resolvedIP!,
{
method: 'PUT',
body: new Uint8Array(fileBuffer),
}
)
if (!uploadResponse.ok) {
logger.error(`[${requestId}] Receipt upload to pre-signed URL failed:`, {
status: uploadResponse.status,
})
return NextResponse.json(
{ success: false, error: `Failed to upload receipt file (${uploadResponse.status})` },
{ status: 502 }
)
}
logger.info(`[${requestId}] Receipt uploaded successfully (ID: ${createData.id})`)
return NextResponse.json({
success: true,
output: {
receiptId: createData.id,
receiptName: effectiveReceiptName,
expenseId: expenseId ?? null,
},
})
} catch (error) {
logger.error(`[${requestId}] Unexpected error:`, error)
return NextResponse.json(
{ success: false, error: getErrorMessage(error, 'Unknown error') },
{ status: 500 }
)
}
})
@@ -0,0 +1,86 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { calcomEventTypesSelectorContract } from '@/lib/api/contracts/selectors'
import { parseRequest } from '@/lib/api/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
const logger = createLogger('CalcomEventTypesAPI')
export const dynamic = 'force-dynamic'
interface CalcomEventType {
id: number
title: string
slug: string
}
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const parsed = await parseRequest(calcomEventTypesSelectorContract, request, {})
if (!parsed.success) return parsed.response
const { credential, workflowId } = parsed.data.body
const authz = await authorizeCredentialUse(request, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{ error: 'Could not retrieve access token', authRequired: true },
{ status: 401 }
)
}
const response = await fetch('https://api.cal.com/v2/event-types', {
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'cal-api-version': '2024-06-14',
},
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
logger.error('Failed to fetch Cal.com event types', {
status: response.status,
error: errorData,
})
return NextResponse.json(
{ error: 'Failed to fetch Cal.com event types', details: errorData },
{ status: response.status }
)
}
const data = (await response.json()) as { data?: CalcomEventType[] }
const eventTypes = (data.data || []).map((eventType) => ({
id: String(eventType.id),
title: eventType.title,
slug: eventType.slug,
}))
return NextResponse.json({ eventTypes })
} catch (error) {
logger.error('Error processing Cal.com event types request:', error)
return NextResponse.json(
{ error: 'Failed to retrieve Cal.com event types', details: (error as Error).message },
{ status: 500 }
)
}
})
@@ -0,0 +1,84 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { calcomSchedulesSelectorContract } from '@/lib/api/contracts/selectors'
import { parseRequest } from '@/lib/api/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
const logger = createLogger('CalcomSchedulesAPI')
export const dynamic = 'force-dynamic'
interface CalcomSchedule {
id: number
name: string
}
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()
try {
const parsed = await parseRequest(calcomSchedulesSelectorContract, request, {})
if (!parsed.success) return parsed.response
const { credential, workflowId } = parsed.data.body
const authz = await authorizeCredentialUse(request, {
credentialId: credential,
workflowId,
})
if (!authz.ok || !authz.credentialOwnerUserId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}
const accessToken = await refreshAccessTokenIfNeeded(
credential,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
logger.error('Failed to get access token', {
credentialId: credential,
userId: authz.credentialOwnerUserId,
})
return NextResponse.json(
{ error: 'Could not retrieve access token', authRequired: true },
{ status: 401 }
)
}
const response = await fetch('https://api.cal.com/v2/schedules', {
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'cal-api-version': '2024-06-11',
},
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
logger.error('Failed to fetch Cal.com schedules', {
status: response.status,
error: errorData,
})
return NextResponse.json(
{ error: 'Failed to fetch Cal.com schedules', details: errorData },
{ status: response.status }
)
}
const data = (await response.json()) as { data?: CalcomSchedule[] }
const schedules = (data.data || []).map((schedule) => ({
id: String(schedule.id),
name: schedule.name,
}))
return NextResponse.json({ schedules })
} catch (error) {
logger.error('Error processing Cal.com schedules request:', error)
return NextResponse.json(
{ error: 'Failed to retrieve Cal.com schedules', details: (error as Error).message },
{ status: 500 }
)
}
})
@@ -0,0 +1,42 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseCountRowsContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseCountRows } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseCountRowsAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse count rows attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseCountRowsContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
const count = await executeClickHouseCountRows(params, params.table, params.where)
return NextResponse.json({
message: `Table contains ${count} row(s).`,
count,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse count rows failed:`, error)
return NextResponse.json(
{ error: `ClickHouse count rows failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,43 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseCreateDatabaseContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseCreateDatabase } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseCreateDatabaseAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse create database attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseCreateDatabaseContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
await executeClickHouseCreateDatabase(params, params.name)
return NextResponse.json({
message: `Database '${params.name}' created.`,
rows: [],
rowCount: 0,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse create database failed:`, error)
return NextResponse.json(
{ error: `ClickHouse create database failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,50 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseCreateTableContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseCreateTable } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseCreateTableAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse create table attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseCreateTableContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
await executeClickHouseCreateTable(
params,
params.table,
params.columns,
params.engine,
params.orderBy,
params.partitionBy
)
return NextResponse.json({
message: `Table '${params.table}' created.`,
rows: [],
rowCount: 0,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse create table failed:`, error)
return NextResponse.json(
{ error: `ClickHouse create table failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,49 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseDeleteContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseDelete } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseDeleteAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse delete attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseDeleteContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(
`[${requestId}] Deleting data from ${params.table} on ${params.host}:${params.port}/${params.database}`
)
const result = await executeClickHouseDelete(params, params.table, params.where)
logger.info(`[${requestId}] Delete mutation submitted, ${result.rowCount} row(s) affected`)
return NextResponse.json({
message: `Delete mutation submitted. ClickHouse mutations run asynchronously. ${result.rowCount} row(s) affected.`,
rows: result.rows,
rowCount: result.rowCount,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse delete failed:`, error)
return NextResponse.json(
{ error: `ClickHouse delete failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,43 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseDescribeTableContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseDescribeTable } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseDescribeTableAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse describe table attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseDescribeTableContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
const result = await executeClickHouseDescribeTable(params, params.table)
return NextResponse.json({
message: `Described table with ${result.rowCount} column(s).`,
rows: result.rows,
rowCount: result.rowCount,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse describe table failed:`, error)
return NextResponse.json(
{ error: `ClickHouse describe table failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,43 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseDropDatabaseContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseDropDatabase } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseDropDatabaseAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse drop database attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseDropDatabaseContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
await executeClickHouseDropDatabase(params, params.name)
return NextResponse.json({
message: `Database '${params.name}' dropped.`,
rows: [],
rowCount: 0,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse drop database failed:`, error)
return NextResponse.json(
{ error: `ClickHouse drop database failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,43 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseDropPartitionContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseDropPartition } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseDropPartitionAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse drop partition attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseDropPartitionContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
await executeClickHouseDropPartition(params, params.table, params.partition)
return NextResponse.json({
message: `Dropped partition from table '${params.table}'.`,
rows: [],
rowCount: 0,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse drop partition failed:`, error)
return NextResponse.json(
{ error: `ClickHouse drop partition failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,43 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseDropTableContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseDropTable } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseDropTableAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse drop table attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseDropTableContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
await executeClickHouseDropTable(params, params.table)
return NextResponse.json({
message: `Table '${params.table}' dropped.`,
rows: [],
rowCount: 0,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse drop table failed:`, error)
return NextResponse.json(
{ error: `ClickHouse drop table failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,49 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseExecuteContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseQuery } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseExecuteAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse execute attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseExecuteContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(
`[${requestId}] Executing ClickHouse statement on ${params.host}:${params.port}/${params.database}`
)
const result = await executeClickHouseQuery(params, params.query)
logger.info(`[${requestId}] Statement executed successfully, ${result.rowCount} row(s)`)
return NextResponse.json({
message: `Statement executed successfully. ${result.rowCount} row(s) returned or affected.`,
rows: result.rows,
rowCount: result.rowCount,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse execute failed:`, error)
return NextResponse.json(
{ error: `ClickHouse execute failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,43 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseInsertRowsContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseInsertRows } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseInsertRowsAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse insert rows attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseInsertRowsContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
const result = await executeClickHouseInsertRows(params, params.table, params.rows)
return NextResponse.json({
message: `Inserted ${result.rowCount} row(s) into '${params.table}'.`,
rows: result.rows,
rowCount: result.rowCount,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse insert rows failed:`, error)
return NextResponse.json(
{ error: `ClickHouse insert rows failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,49 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseInsertContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseInsert } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseInsertAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse insert attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseInsertContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(
`[${requestId}] Inserting data into ${params.table} on ${params.host}:${params.port}/${params.database}`
)
const result = await executeClickHouseInsert(params, params.table, params.data)
logger.info(`[${requestId}] Insert executed successfully, ${result.rowCount} row(s) inserted`)
return NextResponse.json({
message: `Data inserted successfully. ${result.rowCount} row(s) affected.`,
rows: result.rows,
rowCount: result.rowCount,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse insert failed:`, error)
return NextResponse.json(
{ error: `ClickHouse insert failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,50 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseIntrospectContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseIntrospect } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseIntrospectAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse introspect attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseIntrospectContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
logger.info(
`[${requestId}] Introspecting ClickHouse schema on ${params.host}:${params.port}/${params.database}`
)
const result = await executeClickHouseIntrospect(params)
logger.info(
`[${requestId}] Introspection completed successfully, found ${result.tables.length} tables`
)
return NextResponse.json({
message: `Schema introspection completed. Found ${result.tables.length} table(s) in database '${params.database}'.`,
tables: result.tables,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse introspection failed:`, error)
return NextResponse.json(
{ error: `ClickHouse introspection failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,43 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseKillQueryContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseKillQuery } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseKillQueryAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse kill query attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseKillQueryContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
const result = await executeClickHouseKillQuery(params, params.queryId)
return NextResponse.json({
message: `Kill command executed for query '${params.queryId}'.`,
rows: result.rows,
rowCount: result.rowCount,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse kill query failed:`, error)
return NextResponse.json(
{ error: `ClickHouse kill query failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,43 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseListClustersContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseListClusters } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseListClustersAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse list clusters attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseListClustersContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
const result = await executeClickHouseListClusters(params)
return NextResponse.json({
message: `Found ${result.rowCount} cluster node(s).`,
rows: result.rows,
rowCount: result.rowCount,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse list clusters failed:`, error)
return NextResponse.json(
{ error: `ClickHouse list clusters failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,43 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseListDatabasesContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseListDatabases } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseListDatabasesAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse list databases attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseListDatabasesContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
const result = await executeClickHouseListDatabases(params)
return NextResponse.json({
message: `Found ${result.rowCount} database(s).`,
rows: result.rows,
rowCount: result.rowCount,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse list databases failed:`, error)
return NextResponse.json(
{ error: `ClickHouse list databases failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,43 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseListMutationsContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseListMutations } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseListMutationsAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse list mutations attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseListMutationsContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
const result = await executeClickHouseListMutations(params, params.table, params.onlyRunning)
return NextResponse.json({
message: `Found ${result.rowCount} mutation(s).`,
rows: result.rows,
rowCount: result.rowCount,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse list mutations failed:`, error)
return NextResponse.json(
{ error: `ClickHouse list mutations failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,43 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseListPartitionsContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseListPartitions } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseListPartitionsAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse list partitions attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseListPartitionsContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
const result = await executeClickHouseListPartitions(params, params.table)
return NextResponse.json({
message: `Found ${result.rowCount} partition(s).`,
rows: result.rows,
rowCount: result.rowCount,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse list partitions failed:`, error)
return NextResponse.json(
{ error: `ClickHouse list partitions failed: ${errorMessage}` },
{ status: 500 }
)
}
})
@@ -0,0 +1,43 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { clickhouseListRunningQueriesContract } from '@/lib/api/contracts/tools/databases/clickhouse'
import { parseToolRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeClickHouseListRunningQueries } from '@/app/api/tools/clickhouse/utils'
const logger = createLogger('ClickHouseListRunningQueriesAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const requestId = generateId().slice(0, 8)
try {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized ClickHouse list running queries attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseToolRequest(clickhouseListRunningQueriesContract, request, { logger })
if (!parsed.success) return parsed.response
const params = parsed.data.body
const result = await executeClickHouseListRunningQueries(params)
return NextResponse.json({
message: `Found ${result.rowCount} running query(ies).`,
rows: result.rows,
rowCount: result.rowCount,
})
} catch (error) {
const errorMessage = getErrorMessage(error, 'Unknown error occurred')
logger.error(`[${requestId}] ClickHouse list running queries failed:`, error)
return NextResponse.json(
{ error: `ClickHouse list running queries failed: ${errorMessage}` },
{ status: 500 }
)
}
})

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