d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
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
602 lines
20 KiB
TypeScript
602 lines
20 KiB
TypeScript
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
|
|
import { db } from '@sim/db'
|
|
import { webhook, workflow, workflowDeploymentVersion } from '@sim/db/schema'
|
|
import { createLogger } from '@sim/logger'
|
|
import {
|
|
assertWorkflowMutable,
|
|
authorizeWorkflowByWorkspacePermission,
|
|
WorkflowLockedError,
|
|
} from '@sim/platform-authz/workflow'
|
|
import { getErrorMessage } from '@sim/utils/errors'
|
|
import { generateId, generateShortId } from '@sim/utils/id'
|
|
import { and, desc, eq, inArray, isNull, or } from 'drizzle-orm'
|
|
import { type NextRequest, NextResponse } from 'next/server'
|
|
import { listWebhooksContract, upsertWebhookContract } from '@/lib/api/contracts/webhooks'
|
|
import { parseRequest } from '@/lib/api/server'
|
|
import { getSession } from '@/lib/auth'
|
|
import { PlatformEvents } from '@/lib/core/telemetry'
|
|
import { generateRequestId } from '@/lib/core/utils/request'
|
|
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
|
import { captureServerEvent } from '@/lib/posthog/server'
|
|
import { resolveEnvVarsInObject } from '@/lib/webhooks/env-resolver'
|
|
import {
|
|
cleanupExternalWebhook,
|
|
createExternalWebhookSubscription,
|
|
shouldRecreateExternalWebhookSubscription,
|
|
} from '@/lib/webhooks/provider-subscriptions'
|
|
import { getProviderHandler } from '@/lib/webhooks/providers'
|
|
import { mergeNonUserFields } from '@/lib/webhooks/utils'
|
|
import { findConflictingWebhookPathOwner } from '@/lib/webhooks/utils.server'
|
|
import { listAccessibleWorkspaceRowsForUser } from '@/lib/workspaces/utils'
|
|
|
|
const logger = createLogger('WebhooksAPI')
|
|
|
|
export const dynamic = 'force-dynamic'
|
|
|
|
async function revertSavedWebhook(
|
|
savedWebhook: any,
|
|
existingWebhook: any,
|
|
requestId: string
|
|
): Promise<void> {
|
|
if (existingWebhook) {
|
|
await db
|
|
.update(webhook)
|
|
.set({
|
|
workflowId: existingWebhook.workflowId,
|
|
blockId: existingWebhook.blockId,
|
|
path: existingWebhook.path,
|
|
provider: existingWebhook.provider,
|
|
providerConfig: existingWebhook.providerConfig,
|
|
isActive: existingWebhook.isActive,
|
|
archivedAt: existingWebhook.archivedAt,
|
|
updatedAt: existingWebhook.updatedAt,
|
|
})
|
|
.where(eq(webhook.id, savedWebhook.id))
|
|
logger.info(`[${requestId}] Restored previous webhook configuration after failed re-save`, {
|
|
webhookId: savedWebhook.id,
|
|
})
|
|
return
|
|
}
|
|
|
|
await db.delete(webhook).where(eq(webhook.id, savedWebhook.id))
|
|
}
|
|
|
|
// Get all webhooks for the current user
|
|
export const GET = withRouteHandler(async (request: NextRequest) => {
|
|
const requestId = generateRequestId()
|
|
|
|
try {
|
|
const session = await getSession()
|
|
if (!session?.user?.id) {
|
|
logger.warn(`[${requestId}] Unauthorized webhooks access attempt`)
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
}
|
|
|
|
const parsed = await parseRequest(listWebhooksContract, request, {})
|
|
if (!parsed.success) return parsed.response
|
|
const { workflowId, blockId } = parsed.data.query
|
|
|
|
if (workflowId && blockId) {
|
|
// Collaborative-aware path: allow collaborators with read access to view webhooks
|
|
// Fetch workflow to verify access
|
|
const wf = await db
|
|
.select({ id: workflow.id, userId: workflow.userId, workspaceId: workflow.workspaceId })
|
|
.from(workflow)
|
|
.where(eq(workflow.id, workflowId))
|
|
.limit(1)
|
|
|
|
if (!wf.length) {
|
|
logger.warn(`[${requestId}] Workflow not found: ${workflowId}`)
|
|
return NextResponse.json({ error: 'Workflow not found' }, { status: 404 })
|
|
}
|
|
|
|
const wfRecord = wf[0]
|
|
const authorization = await authorizeWorkflowByWorkspacePermission({
|
|
workflowId: wfRecord.id,
|
|
userId: session.user.id,
|
|
action: 'read',
|
|
})
|
|
const canRead = authorization.allowed
|
|
|
|
if (!canRead) {
|
|
logger.warn(
|
|
`[${requestId}] User ${session.user.id} denied permission to read webhooks for workflow ${workflowId}`
|
|
)
|
|
return NextResponse.json({ webhooks: [] }, { status: 200 })
|
|
}
|
|
|
|
const webhooks = await db
|
|
.select({
|
|
webhook: webhook,
|
|
workflow: {
|
|
id: workflow.id,
|
|
name: workflow.name,
|
|
},
|
|
})
|
|
.from(webhook)
|
|
.innerJoin(workflow, eq(webhook.workflowId, workflow.id))
|
|
.leftJoin(
|
|
workflowDeploymentVersion,
|
|
and(
|
|
eq(workflowDeploymentVersion.workflowId, workflow.id),
|
|
eq(workflowDeploymentVersion.isActive, true)
|
|
)
|
|
)
|
|
.where(
|
|
and(
|
|
eq(webhook.workflowId, workflowId),
|
|
eq(webhook.blockId, blockId),
|
|
isNull(webhook.archivedAt),
|
|
or(
|
|
eq(webhook.deploymentVersionId, workflowDeploymentVersion.id),
|
|
and(isNull(workflowDeploymentVersion.id), isNull(webhook.deploymentVersionId))
|
|
)
|
|
)
|
|
)
|
|
.orderBy(desc(webhook.updatedAt))
|
|
|
|
logger.info(
|
|
`[${requestId}] Retrieved ${webhooks.length} webhooks for workflow ${workflowId} block ${blockId}`
|
|
)
|
|
return NextResponse.json({ webhooks }, { status: 200 })
|
|
}
|
|
|
|
if (workflowId && !blockId) {
|
|
// For now, allow the call but return empty results to avoid breaking the UI
|
|
return NextResponse.json({ webhooks: [] }, { status: 200 })
|
|
}
|
|
|
|
const accessibleRows = await listAccessibleWorkspaceRowsForUser(session.user.id, 'all')
|
|
const workspaceIds = accessibleRows.map((row) => row.workspace.id)
|
|
if (workspaceIds.length === 0) {
|
|
return NextResponse.json({ webhooks: [] }, { status: 200 })
|
|
}
|
|
|
|
const webhooks = await db
|
|
.select({
|
|
webhook: webhook,
|
|
workflow: {
|
|
id: workflow.id,
|
|
name: workflow.name,
|
|
},
|
|
})
|
|
.from(webhook)
|
|
.innerJoin(workflow, eq(webhook.workflowId, workflow.id))
|
|
.where(and(inArray(workflow.workspaceId, workspaceIds), isNull(webhook.archivedAt)))
|
|
|
|
logger.info(`[${requestId}] Retrieved ${webhooks.length} workspace-accessible webhooks`)
|
|
return NextResponse.json({ webhooks }, { status: 200 })
|
|
} catch (error) {
|
|
logger.error(`[${requestId}] Error fetching webhooks`, error)
|
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
|
}
|
|
})
|
|
|
|
// Create or Update a webhook
|
|
export const POST = withRouteHandler(async (request: NextRequest) => {
|
|
const requestId = generateRequestId()
|
|
const session = await getSession()
|
|
const userId = session?.user?.id
|
|
|
|
if (!userId) {
|
|
logger.warn(`[${requestId}] Unauthorized webhook creation attempt`)
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
}
|
|
|
|
try {
|
|
const parsed = await parseRequest(upsertWebhookContract, request, {})
|
|
if (!parsed.success) return parsed.response
|
|
|
|
const body = parsed.data.body
|
|
const { workflowId, path, providerConfig, blockId } = body
|
|
const provider = body.provider || ''
|
|
|
|
// Validate input
|
|
if (!workflowId) {
|
|
logger.warn(`[${requestId}] Missing required fields for webhook creation`, {
|
|
hasWorkflowId: !!workflowId,
|
|
hasPath: !!path,
|
|
})
|
|
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 })
|
|
}
|
|
|
|
// Determine final path with special handling for credential-based providers
|
|
// to avoid generating a new path on every save.
|
|
let finalPath = path
|
|
const credentialBasedProviders = ['gmail', 'outlook']
|
|
const isCredentialBased = credentialBasedProviders.includes(provider)
|
|
// Treat Microsoft Teams chat subscription as credential-based for path generation purposes
|
|
const isMicrosoftTeamsChatSubscription =
|
|
provider === 'microsoft-teams' &&
|
|
typeof providerConfig === 'object' &&
|
|
providerConfig?.triggerId === 'microsoftteams_chat_subscription'
|
|
|
|
// If path is missing
|
|
if (!finalPath || finalPath.trim() === '') {
|
|
if (isCredentialBased || isMicrosoftTeamsChatSubscription) {
|
|
// Try to reuse existing path for this workflow+block if one exists
|
|
if (blockId) {
|
|
const existingForBlock = await db
|
|
.select({ id: webhook.id, path: webhook.path })
|
|
.from(webhook)
|
|
.leftJoin(
|
|
workflowDeploymentVersion,
|
|
and(
|
|
eq(workflowDeploymentVersion.workflowId, workflowId),
|
|
eq(workflowDeploymentVersion.isActive, true)
|
|
)
|
|
)
|
|
.where(
|
|
and(
|
|
eq(webhook.workflowId, workflowId),
|
|
eq(webhook.blockId, blockId),
|
|
isNull(webhook.archivedAt),
|
|
or(
|
|
eq(webhook.deploymentVersionId, workflowDeploymentVersion.id),
|
|
and(isNull(workflowDeploymentVersion.id), isNull(webhook.deploymentVersionId))
|
|
)
|
|
)
|
|
)
|
|
.limit(1)
|
|
|
|
if (existingForBlock.length > 0) {
|
|
finalPath = existingForBlock[0].path ?? ''
|
|
logger.info(
|
|
`[${requestId}] Reusing existing generated path for ${provider} trigger: ${finalPath}`
|
|
)
|
|
}
|
|
}
|
|
|
|
// If still no path, generate a new dummy path (first-time save)
|
|
if (!finalPath || finalPath.trim() === '') {
|
|
finalPath = `${provider}-${generateId()}`
|
|
logger.info(`[${requestId}] Generated webhook path for ${provider} trigger: ${finalPath}`)
|
|
}
|
|
} else {
|
|
logger.warn(`[${requestId}] Missing path for webhook creation`, {
|
|
hasWorkflowId: !!workflowId,
|
|
hasPath: !!path,
|
|
})
|
|
return NextResponse.json({ error: 'Missing required path' }, { status: 400 })
|
|
}
|
|
}
|
|
|
|
// Check if the workflow exists and user has permission to modify it
|
|
const workflowData = await db
|
|
.select({
|
|
id: workflow.id,
|
|
userId: workflow.userId,
|
|
workspaceId: workflow.workspaceId,
|
|
})
|
|
.from(workflow)
|
|
.where(eq(workflow.id, workflowId))
|
|
.limit(1)
|
|
|
|
if (workflowData.length === 0) {
|
|
logger.warn(`[${requestId}] Workflow not found: ${workflowId}`)
|
|
return NextResponse.json({ error: 'Workflow not found' }, { status: 404 })
|
|
}
|
|
|
|
const workflowRecord = workflowData[0]
|
|
|
|
const authorization = await authorizeWorkflowByWorkspacePermission({
|
|
workflowId,
|
|
userId,
|
|
action: 'write',
|
|
})
|
|
const canModify = authorization.allowed
|
|
|
|
if (!canModify) {
|
|
logger.warn(
|
|
`[${requestId}] User ${userId} denied permission to modify webhook for workflow ${workflowId}`
|
|
)
|
|
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
|
|
}
|
|
await assertWorkflowMutable(workflowId)
|
|
|
|
// Determine existing webhook to update (prefer by workflow+block for credential-based providers)
|
|
let targetWebhookId: string | null = null
|
|
if (isCredentialBased && blockId) {
|
|
const existingForBlock = await db
|
|
.select({ id: webhook.id })
|
|
.from(webhook)
|
|
.leftJoin(
|
|
workflowDeploymentVersion,
|
|
and(
|
|
eq(workflowDeploymentVersion.workflowId, workflowId),
|
|
eq(workflowDeploymentVersion.isActive, true)
|
|
)
|
|
)
|
|
.where(
|
|
and(
|
|
eq(webhook.workflowId, workflowId),
|
|
eq(webhook.blockId, blockId),
|
|
isNull(webhook.archivedAt),
|
|
or(
|
|
eq(webhook.deploymentVersionId, workflowDeploymentVersion.id),
|
|
and(isNull(workflowDeploymentVersion.id), isNull(webhook.deploymentVersionId))
|
|
)
|
|
)
|
|
)
|
|
.limit(1)
|
|
if (existingForBlock.length > 0) {
|
|
targetWebhookId = existingForBlock[0].id
|
|
}
|
|
}
|
|
if (!targetWebhookId) {
|
|
const conflictingOwner = await findConflictingWebhookPathOwner({
|
|
path: finalPath,
|
|
workflowId,
|
|
})
|
|
if (conflictingOwner) {
|
|
logger.warn(`[${requestId}] Webhook path conflict: ${finalPath}`)
|
|
return NextResponse.json(
|
|
{ error: 'Webhook path already exists.', code: 'PATH_EXISTS' },
|
|
{ status: 409 }
|
|
)
|
|
}
|
|
|
|
const ownExisting = await db
|
|
.select({ id: webhook.id })
|
|
.from(webhook)
|
|
.where(
|
|
and(
|
|
eq(webhook.path, finalPath),
|
|
eq(webhook.workflowId, workflowId),
|
|
isNull(webhook.archivedAt)
|
|
)
|
|
)
|
|
.limit(1)
|
|
if (ownExisting.length > 0) {
|
|
targetWebhookId = ownExisting[0].id
|
|
}
|
|
}
|
|
|
|
let savedWebhook: any = null
|
|
let existingWebhook: any = null
|
|
const originalProviderConfig = providerConfig || {}
|
|
let resolvedProviderConfig = await resolveEnvVarsInObject(
|
|
originalProviderConfig,
|
|
userId,
|
|
workflowRecord.workspaceId || undefined
|
|
)
|
|
|
|
let externalSubscriptionCreated = false
|
|
const createTempWebhookData = (providerConfigOverride = resolvedProviderConfig) => ({
|
|
id: targetWebhookId || generateShortId(),
|
|
path: finalPath,
|
|
provider,
|
|
providerConfig: providerConfigOverride,
|
|
})
|
|
|
|
const userProvided = originalProviderConfig as Record<string, unknown>
|
|
const configToSave: Record<string, unknown> = { ...userProvided }
|
|
|
|
if (targetWebhookId) {
|
|
const existingRows = await db
|
|
.select()
|
|
.from(webhook)
|
|
.where(eq(webhook.id, targetWebhookId))
|
|
.limit(1)
|
|
existingWebhook = existingRows[0] || null
|
|
}
|
|
|
|
const shouldRecreateSubscription =
|
|
existingWebhook &&
|
|
shouldRecreateExternalWebhookSubscription({
|
|
previousProvider: existingWebhook.provider as string,
|
|
nextProvider: provider,
|
|
previousConfig: ((existingWebhook.providerConfig as Record<string, unknown>) ||
|
|
{}) as Record<string, unknown>,
|
|
nextConfig: resolvedProviderConfig,
|
|
})
|
|
|
|
if (!existingWebhook || shouldRecreateSubscription) {
|
|
try {
|
|
const result = await createExternalWebhookSubscription(
|
|
request,
|
|
createTempWebhookData(),
|
|
workflowRecord,
|
|
userId,
|
|
requestId
|
|
)
|
|
const updatedConfig = result.updatedProviderConfig as Record<string, unknown>
|
|
mergeNonUserFields(configToSave, updatedConfig, userProvided)
|
|
resolvedProviderConfig = updatedConfig
|
|
externalSubscriptionCreated = result.externalSubscriptionCreated
|
|
} catch (err) {
|
|
logger.error(`[${requestId}] Error creating external webhook subscription`, err)
|
|
return NextResponse.json(
|
|
{
|
|
error: 'Failed to create external webhook subscription',
|
|
details: getErrorMessage(err, 'Unknown error'),
|
|
},
|
|
{ status: 500 }
|
|
)
|
|
}
|
|
} else {
|
|
mergeNonUserFields(
|
|
configToSave,
|
|
(existingWebhook.providerConfig as Record<string, unknown>) || {},
|
|
userProvided
|
|
)
|
|
}
|
|
|
|
try {
|
|
if (targetWebhookId) {
|
|
logger.info(`[${requestId}] Updating existing webhook for path: ${finalPath}`, {
|
|
webhookId: targetWebhookId,
|
|
provider,
|
|
hasCredentialId: !!(configToSave as any)?.credentialId,
|
|
credentialId: (configToSave as any)?.credentialId,
|
|
})
|
|
const updatedResult = await db
|
|
.update(webhook)
|
|
.set({
|
|
blockId,
|
|
provider,
|
|
providerConfig: configToSave,
|
|
isActive: true,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(webhook.id, targetWebhookId))
|
|
.returning()
|
|
savedWebhook = updatedResult[0]
|
|
logger.info(`[${requestId}] Webhook updated successfully`, {
|
|
webhookId: savedWebhook.id,
|
|
savedProviderConfig: savedWebhook.providerConfig,
|
|
})
|
|
} else {
|
|
// Create a new webhook
|
|
const webhookId = generateShortId()
|
|
logger.info(`[${requestId}] Creating new webhook with ID: ${webhookId}`)
|
|
const newResult = await db
|
|
.insert(webhook)
|
|
.values({
|
|
id: webhookId,
|
|
workflowId,
|
|
blockId,
|
|
path: finalPath,
|
|
provider,
|
|
providerConfig: configToSave,
|
|
isActive: true,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
})
|
|
.returning()
|
|
savedWebhook = newResult[0]
|
|
}
|
|
} catch (dbError) {
|
|
if (externalSubscriptionCreated) {
|
|
logger.error(`[${requestId}] DB save failed, cleaning up external subscription`, dbError)
|
|
try {
|
|
await cleanupExternalWebhook(
|
|
createTempWebhookData(configToSave),
|
|
workflowRecord,
|
|
requestId
|
|
)
|
|
} catch (cleanupError) {
|
|
logger.error(
|
|
`[${requestId}] Failed to cleanup external subscription after DB save failure`,
|
|
cleanupError
|
|
)
|
|
}
|
|
}
|
|
throw dbError
|
|
}
|
|
|
|
if (existingWebhook && shouldRecreateSubscription) {
|
|
try {
|
|
await cleanupExternalWebhook(existingWebhook, workflowRecord, requestId)
|
|
} catch (cleanupError) {
|
|
logger.warn(
|
|
`[${requestId}] Failed to cleanup previous external webhook subscription ${existingWebhook.id}`,
|
|
cleanupError
|
|
)
|
|
}
|
|
}
|
|
|
|
if (savedWebhook) {
|
|
const pollingHandler = getProviderHandler(provider)
|
|
if (pollingHandler.configurePolling) {
|
|
logger.info(
|
|
`[${requestId}] ${provider} provider detected. Setting up polling configuration.`
|
|
)
|
|
try {
|
|
const success = await pollingHandler.configurePolling({
|
|
webhook: savedWebhook,
|
|
requestId,
|
|
})
|
|
|
|
if (!success) {
|
|
logger.error(
|
|
`[${requestId}] Failed to configure ${provider} polling, rolling back webhook`
|
|
)
|
|
await revertSavedWebhook(savedWebhook, existingWebhook, requestId)
|
|
return NextResponse.json(
|
|
{
|
|
error: `Failed to configure ${provider} polling`,
|
|
details: 'Please check your account permissions and try again',
|
|
},
|
|
{ status: 500 }
|
|
)
|
|
}
|
|
|
|
logger.info(`[${requestId}] Successfully configured ${provider} polling`)
|
|
} catch (err) {
|
|
logger.error(
|
|
`[${requestId}] Error setting up ${provider} webhook configuration, rolling back webhook`,
|
|
err
|
|
)
|
|
await revertSavedWebhook(savedWebhook, existingWebhook, requestId)
|
|
return NextResponse.json(
|
|
{
|
|
error: `Failed to configure ${provider} webhook`,
|
|
details: getErrorMessage(err, 'Unknown error'),
|
|
},
|
|
{ status: 500 }
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!targetWebhookId && savedWebhook) {
|
|
try {
|
|
PlatformEvents.webhookCreated({
|
|
webhookId: savedWebhook.id,
|
|
workflowId: workflowId,
|
|
provider: provider || 'generic',
|
|
workspaceId: workflowRecord.workspaceId || undefined,
|
|
})
|
|
} catch {
|
|
// Telemetry should not fail the operation
|
|
}
|
|
|
|
recordAudit({
|
|
workspaceId: workflowRecord.workspaceId || null,
|
|
actorId: userId,
|
|
actorName: session?.user?.name ?? undefined,
|
|
actorEmail: session?.user?.email ?? undefined,
|
|
action: AuditAction.WEBHOOK_CREATED,
|
|
resourceType: AuditResourceType.WEBHOOK,
|
|
resourceId: savedWebhook.id,
|
|
resourceName: provider || 'generic',
|
|
description: `Created ${provider || 'generic'} webhook`,
|
|
metadata: {
|
|
provider: provider || 'generic',
|
|
workflowId,
|
|
webhookPath: finalPath,
|
|
blockId: blockId || undefined,
|
|
},
|
|
request,
|
|
})
|
|
|
|
const wsId = workflowRecord.workspaceId || undefined
|
|
captureServerEvent(
|
|
userId,
|
|
'webhook_trigger_created',
|
|
{
|
|
webhook_id: savedWebhook.id,
|
|
workflow_id: workflowId,
|
|
provider: provider || 'generic',
|
|
workspace_id: wsId ?? '',
|
|
},
|
|
wsId ? { groups: { workspace: wsId } } : undefined
|
|
)
|
|
}
|
|
|
|
const status = targetWebhookId ? 200 : 201
|
|
return NextResponse.json({ webhook: savedWebhook }, { status })
|
|
} catch (error: any) {
|
|
if (error instanceof WorkflowLockedError) {
|
|
return NextResponse.json({ error: error.message }, { status: error.status })
|
|
}
|
|
|
|
logger.error(`[${requestId}] Error creating/updating webhook`, {
|
|
message: error.message,
|
|
stack: error.stack,
|
|
})
|
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
|
}
|
|
})
|