diff --git a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts index b9c0b4f568..bb32950001 100644 --- a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts @@ -10,7 +10,7 @@ interface ToolRuntimeSchemaEntry { } export const TOOL_RUNTIME_SCHEMAS: Record = { - ['agent']: { + agent: { parameters: { properties: { request: { @@ -23,7 +23,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['auth']: { + auth: { parameters: { properties: { request: { @@ -36,7 +36,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['check_deployment_status']: { + check_deployment_status: { parameters: { type: 'object', properties: { @@ -48,7 +48,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['complete_job']: { + complete_job: { parameters: { type: 'object', properties: { @@ -61,7 +61,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['context_write']: { + context_write: { parameters: { type: 'object', properties: { @@ -78,7 +78,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['crawl_website']: { + crawl_website: { parameters: { type: 'object', properties: { @@ -113,7 +113,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['create_file']: { + create_file: { parameters: { type: 'object', properties: { @@ -149,7 +149,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['create_folder']: { + create_folder: { parameters: { type: 'object', properties: { @@ -170,7 +170,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['create_job']: { + create_job: { parameters: { type: 'object', properties: { @@ -220,7 +220,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['create_workflow']: { + create_workflow: { parameters: { type: 'object', properties: { @@ -245,7 +245,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['create_workspace_mcp_server']: { + create_workspace_mcp_server: { parameters: { type: 'object', properties: { @@ -266,7 +266,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['debug']: { + debug: { parameters: { properties: { context: { @@ -285,7 +285,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['delete_file']: { + delete_file: { parameters: { type: 'object', properties: { @@ -314,7 +314,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['delete_folder']: { + delete_folder: { parameters: { type: 'object', properties: { @@ -330,7 +330,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['delete_workflow']: { + delete_workflow: { parameters: { type: 'object', properties: { @@ -346,7 +346,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['delete_workspace_mcp_server']: { + delete_workspace_mcp_server: { parameters: { type: 'object', properties: { @@ -359,7 +359,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['deploy']: { + deploy: { parameters: { properties: { request: { @@ -373,7 +373,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['deploy_api']: { + deploy_api: { parameters: { type: 'object', properties: { @@ -447,7 +447,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { ], }, }, - ['deploy_chat']: { + deploy_chat: { parameters: { type: 'object', properties: { @@ -595,7 +595,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { ], }, }, - ['deploy_mcp']: { + deploy_mcp: { parameters: { type: 'object', properties: { @@ -711,7 +711,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['deploymentType', 'deploymentStatus'], }, }, - ['download_to_workspace_file']: { + download_to_workspace_file: { parameters: { type: 'object', properties: { @@ -730,7 +730,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['edit_content']: { + edit_content: { parameters: { type: 'object', properties: { @@ -762,7 +762,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['edit_workflow']: { + edit_workflow: { parameters: { type: 'object', properties: { @@ -801,13 +801,13 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['file']: { + file: { parameters: { type: 'object', }, resultSchema: undefined, }, - ['function_execute']: { + function_execute: { parameters: { type: 'object', properties: { @@ -868,7 +868,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['generate_api_key']: { + generate_api_key: { parameters: { type: 'object', properties: { @@ -886,7 +886,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['generate_image']: { + generate_image: { parameters: { type: 'object', properties: { @@ -923,7 +923,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['generate_visualization']: { + generate_visualization: { parameters: { type: 'object', properties: { @@ -963,7 +963,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_block_outputs']: { + get_block_outputs: { parameters: { type: 'object', properties: { @@ -984,7 +984,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_block_upstream_references']: { + get_block_upstream_references: { parameters: { type: 'object', properties: { @@ -1006,7 +1006,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_deployed_workflow_state']: { + get_deployed_workflow_state: { parameters: { type: 'object', properties: { @@ -1019,7 +1019,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_deployment_version']: { + get_deployment_version: { parameters: { type: 'object', properties: { @@ -1036,7 +1036,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_execution_summary']: { + get_execution_summary: { parameters: { type: 'object', properties: { @@ -1063,7 +1063,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_job_logs']: { + get_job_logs: { parameters: { type: 'object', properties: { @@ -1088,7 +1088,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_page_contents']: { + get_page_contents: { parameters: { type: 'object', properties: { @@ -1116,14 +1116,14 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_platform_actions']: { + get_platform_actions: { parameters: { type: 'object', properties: {}, }, resultSchema: undefined, }, - ['get_workflow_data']: { + get_workflow_data: { parameters: { type: 'object', properties: { @@ -1142,7 +1142,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['get_workflow_logs']: { + get_workflow_logs: { parameters: { type: 'object', properties: { @@ -1168,7 +1168,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['glob']: { + glob: { parameters: { type: 'object', properties: { @@ -1187,7 +1187,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['grep']: { + grep: { parameters: { type: 'object', properties: { @@ -1234,7 +1234,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['job']: { + job: { parameters: { properties: { request: { @@ -1247,7 +1247,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['knowledge']: { + knowledge: { parameters: { properties: { request: { @@ -1260,7 +1260,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['knowledge_base']: { + knowledge_base: { parameters: { type: 'object', properties: { @@ -1452,7 +1452,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['list_folders']: { + list_folders: { parameters: { type: 'object', properties: { @@ -1464,14 +1464,14 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['list_user_workspaces']: { + list_user_workspaces: { parameters: { type: 'object', properties: {}, }, resultSchema: undefined, }, - ['list_workspace_mcp_servers']: { + list_workspace_mcp_servers: { parameters: { type: 'object', properties: { @@ -1483,7 +1483,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_credential']: { + manage_credential: { parameters: { type: 'object', properties: { @@ -1512,7 +1512,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_custom_tool']: { + manage_custom_tool: { parameters: { type: 'object', properties: { @@ -1591,7 +1591,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_job']: { + manage_job: { parameters: { type: 'object', properties: { @@ -1661,7 +1661,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_mcp_tool']: { + manage_mcp_tool: { parameters: { type: 'object', properties: { @@ -1712,7 +1712,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['manage_skill']: { + manage_skill: { parameters: { type: 'object', properties: { @@ -1744,7 +1744,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['materialize_file']: { + materialize_file: { parameters: { type: 'object', properties: { @@ -1778,7 +1778,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['move_folder']: { + move_folder: { parameters: { type: 'object', properties: { @@ -1796,7 +1796,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['move_workflow']: { + move_workflow: { parameters: { type: 'object', properties: { @@ -1816,7 +1816,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['oauth_get_auth_link']: { + oauth_get_auth_link: { parameters: { type: 'object', properties: { @@ -1830,7 +1830,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['oauth_request_access']: { + oauth_request_access: { parameters: { type: 'object', properties: { @@ -1844,7 +1844,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['open_resource']: { + open_resource: { parameters: { type: 'object', properties: { @@ -1872,7 +1872,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['read']: { + read: { parameters: { type: 'object', properties: { @@ -1899,7 +1899,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['redeploy']: { + redeploy: { parameters: { type: 'object', properties: { @@ -1967,7 +1967,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { ], }, }, - ['rename_file']: { + rename_file: { parameters: { type: 'object', properties: { @@ -2002,7 +2002,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['rename_workflow']: { + rename_workflow: { parameters: { type: 'object', properties: { @@ -2019,7 +2019,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['research']: { + research: { parameters: { properties: { topic: { @@ -2032,7 +2032,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['respond']: { + respond: { parameters: { additionalProperties: true, properties: { @@ -2055,7 +2055,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['restore_resource']: { + restore_resource: { parameters: { type: 'object', properties: { @@ -2073,7 +2073,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['revert_to_version']: { + revert_to_version: { parameters: { type: 'object', properties: { @@ -2090,7 +2090,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run']: { + run: { parameters: { properties: { context: { @@ -2107,7 +2107,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run_block']: { + run_block: { parameters: { type: 'object', properties: { @@ -2139,7 +2139,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run_from_block']: { + run_from_block: { parameters: { type: 'object', properties: { @@ -2171,7 +2171,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run_workflow']: { + run_workflow: { parameters: { type: 'object', properties: { @@ -2199,7 +2199,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['run_workflow_until_block']: { + run_workflow_until_block: { parameters: { type: 'object', properties: { @@ -2231,7 +2231,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['scrape_page']: { + scrape_page: { parameters: { type: 'object', properties: { @@ -2252,7 +2252,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['search_documentation']: { + search_documentation: { parameters: { type: 'object', properties: { @@ -2269,7 +2269,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['search_library_docs']: { + search_library_docs: { parameters: { type: 'object', properties: { @@ -2290,7 +2290,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['search_online']: { + search_online: { parameters: { type: 'object', properties: { @@ -2331,7 +2331,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['search_patterns']: { + search_patterns: { parameters: { type: 'object', properties: { @@ -2353,7 +2353,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['set_block_enabled']: { + set_block_enabled: { parameters: { type: 'object', properties: { @@ -2375,7 +2375,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['set_environment_variables']: { + set_environment_variables: { parameters: { type: 'object', properties: { @@ -2409,7 +2409,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['set_global_workflow_variables']: { + set_global_workflow_variables: { parameters: { type: 'object', properties: { @@ -2447,7 +2447,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['superagent']: { + superagent: { parameters: { properties: { task: { @@ -2461,7 +2461,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['table']: { + table: { parameters: { properties: { request: { @@ -2474,7 +2474,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['tool_search_tool_regex']: { + tool_search_tool_regex: { parameters: { properties: { case_insensitive: { @@ -2495,7 +2495,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['update_job_history']: { + update_job_history: { parameters: { type: 'object', properties: { @@ -2513,7 +2513,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['update_workspace_mcp_server']: { + update_workspace_mcp_server: { parameters: { type: 'object', properties: { @@ -2538,7 +2538,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['user_memory']: { + user_memory: { parameters: { type: 'object', properties: { @@ -2586,7 +2586,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - ['user_table']: { + user_table: { parameters: { type: 'object', properties: { @@ -2915,13 +2915,13 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - ['workflow']: { + workflow: { parameters: { type: 'object', }, resultSchema: undefined, }, - ['workspace_file']: { + workspace_file: { parameters: { type: 'object', properties: { diff --git a/apps/sim/lib/webhooks/providers/airtable.ts b/apps/sim/lib/webhooks/providers/airtable.ts index fd0463b4b9..ff41a232ac 100644 --- a/apps/sim/lib/webhooks/providers/airtable.ts +++ b/apps/sim/lib/webhooks/providers/airtable.ts @@ -1,7 +1,10 @@ +import crypto from 'crypto' import { db } from '@sim/db' import { account, webhook } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { safeCompare } from '@sim/security/compare' import { eq } from 'drizzle-orm' +import { NextResponse } from 'next/server' import { validateAirtableId } from '@/lib/core/security/input-validation' import { getBaseUrl } from '@/lib/core/utils/urls' import { @@ -10,6 +13,7 @@ import { getProviderConfig, } from '@/lib/webhooks/provider-subscription-utils' import type { + AuthContext, DeleteSubscriptionContext, FormatInputContext, SubscriptionContext, @@ -52,14 +56,11 @@ async function fetchAndProcessAirtablePayloads( workflowData: Record, requestId: string // Original request ID from the ping, used for the final execution log ) { - // Logging handles all error logging let currentCursor: number | null = null let mightHaveMore = true let payloadsFetched = 0 let apiCallCount = 0 - // Use a Map to consolidate changes per record ID const consolidatedChangesMap = new Map() - // Capture raw payloads from Airtable for exposure to workflows const allPayloads = [] const localProviderConfig = { ...((webhookData.providerConfig as Record) || {}), @@ -217,7 +218,6 @@ async function fetchAndProcessAirtablePayloads( error: errorMessage, } ) - // Error logging handled by logging session mightHaveMore = false break } @@ -300,7 +300,6 @@ async function fetchAndProcessAirtablePayloads( } } } - // TODO: Handle deleted records (`destroyedRecordIds`) if needed } } } @@ -312,7 +311,6 @@ async function fetchAndProcessAirtablePayloads( if (nextCursor && typeof nextCursor === 'number' && nextCursor !== currentCursor) { currentCursor = nextCursor - // Follow exactly the old implementation - use awaited update instead of parallel const updatedConfig = { ...localProviderConfig, externalWebhookCursor: currentCursor, @@ -322,7 +320,7 @@ async function fetchAndProcessAirtablePayloads( await db .update(webhook) .set({ - providerConfig: updatedConfig, // Use full object + providerConfig: updatedConfig, updatedAt: new Date(), }) .where(eq(webhook.id, webhookData.id as string)) @@ -335,7 +333,6 @@ async function fetchAndProcessAirtablePayloads( cursor: currentCursor, error: err.message, }) - // Error logging handled by logging session mightHaveMore = false throw new Error('Failed to save Airtable cursor, stopping processing.') // Re-throw to break loop clearly } @@ -354,7 +351,6 @@ async function fetchAndProcessAirtablePayloads( `[${requestId}] Network error calling Airtable GET /payloads (Call ${apiCallCount}) for webhook ${webhookData.id}`, fetchError ) - // Error logging handled by logging session mightHaveMore = false break } @@ -367,7 +363,6 @@ async function fetchAndProcessAirtablePayloads( if (finalConsolidatedChanges.length > 0 || allPayloads.length > 0) { try { - // Build input exposing raw payloads and consolidated changes const latestPayload = allPayloads.length > 0 ? allPayloads[allPayloads.length - 1] : null const input: Record = { payloads: allPayloads, @@ -384,9 +379,8 @@ async function fetchAndProcessAirtablePayloads( }, } - // CRITICAL EXECUTION TRACE POINT logger.info( - `[${requestId}] CRITICAL_TRACE: Beginning workflow execution with ${finalConsolidatedChanges.length} Airtable changes`, + `[${requestId}] Beginning workflow execution with ${finalConsolidatedChanges.length} Airtable changes`, { workflowId: workflowData.id, recordCount: finalConsolidatedChanges.length, @@ -395,8 +389,7 @@ async function fetchAndProcessAirtablePayloads( } ) - // Return the processed input for the trigger.dev task to handle - logger.info(`[${requestId}] CRITICAL_TRACE: Airtable changes processed, returning input`, { + logger.info(`[${requestId}] Airtable changes processed, returning input`, { workflowId: workflowData.id, recordCount: finalConsolidatedChanges.length, rawPayloadCount: allPayloads.length, @@ -406,7 +399,7 @@ async function fetchAndProcessAirtablePayloads( return input } catch (processingError: unknown) { const err = processingError as Error - logger.error(`[${requestId}] CRITICAL_TRACE: Error processing Airtable changes`, { + logger.error(`[${requestId}] Error processing Airtable changes`, { workflowId: workflowData.id, error: err.message, stack: err.stack, @@ -416,8 +409,7 @@ async function fetchAndProcessAirtablePayloads( throw processingError } } else { - // DEBUG: Log when no changes are found - logger.info(`[${requestId}] TRACE: No Airtable changes to process`, { + logger.info(`[${requestId}] No Airtable changes to process`, { workflowId: workflowData.id, apiCallCount, webhookId: webhookData.id, @@ -433,11 +425,56 @@ async function fetchAndProcessAirtablePayloads( error: (error as Error).message, } ) - // Error logging handled by logging session } } export const airtableHandler: WebhookProviderHandler = { + verifyAuth({ request, rawBody, requestId, providerConfig }: AuthContext) { + const macSecretBase64 = providerConfig.macSecretBase64 as string | undefined | null + + if (!macSecretBase64) { + logger.warn( + `[${requestId}] Airtable webhook has no macSecretBase64 in providerConfig — skipping MAC verification. Re-create the webhook to enable signature verification.` + ) + return null + } + + const signature = request.headers.get('X-Airtable-Content-MAC') + if (!signature) { + logger.warn(`[${requestId}] Airtable webhook missing X-Airtable-Content-MAC header`) + return new NextResponse('Unauthorized - Missing Airtable MAC header', { status: 401 }) + } + + const EXPECTED_PREFIX = 'hmac-sha256=' + if (!signature.startsWith(EXPECTED_PREFIX)) { + logger.warn(`[${requestId}] Airtable MAC signature has invalid format`) + return new NextResponse('Unauthorized - Invalid Airtable MAC signature format', { + status: 401, + }) + } + const providedHex = signature.slice(EXPECTED_PREFIX.length) + + try { + const secretBytes = Buffer.from(macSecretBase64, 'base64') + const computedHex = crypto + .createHmac('sha256', secretBytes) + .update(rawBody, 'utf8') + .digest('hex') + + if (!safeCompare(computedHex, providedHex)) { + logger.warn(`[${requestId}] Airtable MAC signature verification failed`) + return new NextResponse('Unauthorized - Invalid Airtable MAC signature', { status: 401 }) + } + } catch (error) { + logger.error(`[${requestId}] Error verifying Airtable MAC signature`, { + error: (error as Error).message, + }) + return new NextResponse('Unauthorized - Signature verification error', { status: 401 }) + } + + return null + }, + async createSubscription({ webhook: webhookRecord, workflow, @@ -554,7 +591,12 @@ export const airtableHandler: WebhookProviderHandler = { airtableWebhookId: responseBody.id, } ) - return { providerConfigUpdates: { externalId: responseBody.id } } + return { + providerConfigUpdates: { + externalId: responseBody.id, + macSecretBase64: responseBody.macSecretBase64 ?? null, + }, + } } catch (error: unknown) { const err = error as Error logger.error( diff --git a/apps/sim/lib/webhooks/providers/hubspot.ts b/apps/sim/lib/webhooks/providers/hubspot.ts index 2591ee4017..7be5056b34 100644 --- a/apps/sim/lib/webhooks/providers/hubspot.ts +++ b/apps/sim/lib/webhooks/providers/hubspot.ts @@ -1,5 +1,9 @@ +import crypto from 'crypto' import { createLogger } from '@sim/logger' +import { safeCompare } from '@sim/security/compare' +import { NextResponse } from 'next/server' import type { + AuthContext, EventMatchContext, FormatInputContext, FormatInputResult, @@ -9,6 +13,47 @@ import type { const logger = createLogger('WebhookProvider:HubSpot') export const hubspotHandler: WebhookProviderHandler = { + verifyAuth({ request, rawBody, requestId, providerConfig }: AuthContext) { + const clientSecret = providerConfig.clientSecret as string | undefined + + if (!clientSecret) { + logger.warn( + `[${requestId}] HubSpot webhook missing clientSecret in providerConfig — rejecting request` + ) + return new NextResponse('Unauthorized - Webhook secret not configured', { status: 401 }) + } + + const signature = request.headers.get('X-HubSpot-Signature') + if (!signature) { + logger.warn(`[${requestId}] HubSpot webhook missing X-HubSpot-Signature header`) + return new NextResponse('Unauthorized - Missing HubSpot signature', { status: 401 }) + } + + try { + /** + * HubSpot v1 signature: SHA-256 of (clientSecret + requestBody), verified against X-HubSpot-Signature. + * v1 is intentionally used for CRM webhook subscriptions — v3 requires the full request URL and method, + * which are not available in verifyAuth. + */ + const computedHash = crypto + .createHash('sha256') + .update(clientSecret + rawBody, 'utf8') + .digest('hex') + + if (!safeCompare(computedHash, signature)) { + logger.warn(`[${requestId}] HubSpot signature verification failed`) + return new NextResponse('Unauthorized - Invalid HubSpot signature', { status: 401 }) + } + } catch (error) { + logger.error(`[${requestId}] Error verifying HubSpot signature`, { + error: (error as Error).message, + }) + return new NextResponse('Unauthorized - Signature verification error', { status: 401 }) + } + + return null + }, + async matchEvent({ webhook, workflow, body, requestId, providerConfig }: EventMatchContext) { const triggerId = providerConfig.triggerId as string | undefined diff --git a/apps/sim/lib/webhooks/providers/webflow.ts b/apps/sim/lib/webhooks/providers/webflow.ts index 7494ae3956..9e4ed72ac3 100644 --- a/apps/sim/lib/webhooks/providers/webflow.ts +++ b/apps/sim/lib/webhooks/providers/webflow.ts @@ -1,8 +1,13 @@ +import crypto from 'crypto' import { createLogger } from '@sim/logger' +import { safeCompare } from '@sim/security/compare' +import { toError } from '@sim/utils/errors' +import { NextResponse } from 'next/server' import { validateAlphanumericId } from '@/lib/core/security/input-validation' import { getBaseUrl } from '@/lib/core/utils/urls' import { getCredentialOwner, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' import type { + AuthContext, DeleteSubscriptionContext, EventFilterContext, FormatInputContext, @@ -16,6 +21,54 @@ import { getOAuthToken, refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/ const logger = createLogger('WebhookProvider:Webflow') export const webflowHandler: WebhookProviderHandler = { + verifyAuth({ request, rawBody, requestId, providerConfig }: AuthContext) { + const secretKey = providerConfig.secretKey as string | undefined | null + + if (!secretKey) { + // Fail-open for existing webhooks created before this change (no secretKey stored) + logger.warn( + `[${requestId}] Webflow webhook missing secretKey in providerConfig — skipping signature verification` + ) + return null + } + + const signature = request.headers.get('x-webflow-signature') + const timestamp = request.headers.get('x-webflow-timestamp') + + if (!signature || !timestamp) { + logger.warn(`[${requestId}] Webflow webhook missing signature or timestamp headers`) + return new NextResponse('Unauthorized - Missing Webflow signature headers', { status: 401 }) + } + + // Replay protection: reject if timestamp is more than 5 minutes old. + // x-webflow-timestamp is Unix milliseconds (e.g. 1722370035277) — compare directly with Date.now(). + const ts = Number.parseInt(timestamp, 10) + if (Number.isNaN(ts) || Date.now() - ts > 5 * 60 * 1000) { + logger.warn(`[${requestId}] Webflow webhook timestamp expired or invalid`) + return new NextResponse('Unauthorized - Webhook timestamp expired', { status: 401 }) + } + + try { + // HMAC-SHA256 of "${timestamp}:${rawBody}" + const computedHash = crypto + .createHmac('sha256', secretKey) + .update(`${timestamp}:${rawBody}`, 'utf8') + .digest('hex') + + if (!safeCompare(computedHash, signature)) { + logger.warn(`[${requestId}] Webflow signature verification failed`) + return new NextResponse('Unauthorized - Invalid Webflow signature', { status: 401 }) + } + } catch (error) { + logger.error(`[${requestId}] Error verifying Webflow signature`, { + error: toError(error).message, + }) + return new NextResponse('Unauthorized - Signature verification error', { status: 401 }) + } + + return null + }, + async createSubscription({ webhook: webhookRecord, workflow, @@ -132,7 +185,12 @@ export const webflowHandler: WebhookProviderHandler = { } ) - return { providerConfigUpdates: { externalId: responseBody.id || responseBody._id } } + return { + providerConfigUpdates: { + externalId: responseBody.id || responseBody._id, + secretKey: responseBody.secretKey, + }, + } } catch (error: unknown) { const err = error as Error logger.error(