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,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 }
)
}
})