Skip to content

Commit b5aed85

Browse files
committed
chore(webhooks): remove extraneous comments from provider handlers
fix(security): store Webflow signing key at subscription creation time - Remove all non-TSDoc inline comments from airtable, hubspot, and webflow providers - Fix Webflow OAuth webhook signature verification gap: secretKey was never stored for OAuth-created webhooks (Webflow returns secretKey only for site-token webhooks). Now resolved at createSubscription time via responseBody.secretKey ?? env.WEBFLOW_CLIENT_SECRET ?? null, so verifyAuth has a single clean code path identical to Stripe and HubSpot - Change Webflow verifyAuth from fail-open to fail-closed on missing secretKey - Replace error as Error cast with toError() in Webflow createSubscription catch - Add inline comment noting x-webflow-timestamp is Unix milliseconds
1 parent 15ef66e commit b5aed85

3 files changed

Lines changed: 10 additions & 35 deletions

File tree

apps/sim/lib/webhooks/providers/airtable.ts

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,10 @@ interface AirtableTableChanges {
4848
destroyedRecordIds?: string[]
4949
}
5050

51-
/**
52-
* Process Airtable payloads
53-
*/
5451
async function fetchAndProcessAirtablePayloads(
5552
webhookData: Record<string, unknown>,
5653
workflowData: Record<string, unknown>,
57-
requestId: string // Original request ID from the ping, used for the final execution log
54+
requestId: string
5855
) {
5956
let currentCursor: number | null = null
6057
let mightHaveMore = true
@@ -180,7 +177,6 @@ async function fetchAndProcessAirtablePayloads(
180177

181178
while (mightHaveMore) {
182179
apiCallCount++
183-
// Safety break
184180
if (apiCallCount > 10) {
185181
mightHaveMore = false
186182
break
@@ -226,7 +222,6 @@ async function fetchAndProcessAirtablePayloads(
226222

227223
if (receivedPayloads.length > 0) {
228224
payloadsFetched += receivedPayloads.length
229-
// Keep the raw payloads for later exposure to the workflow
230225
for (const p of receivedPayloads) {
231226
allPayloads.push(p)
232227
}
@@ -247,14 +242,11 @@ async function fetchAndProcessAirtablePayloads(
247242
)) {
248243
const existingChange = consolidatedChangesMap.get(recordId)
249244
if (existingChange) {
250-
// Record was created and possibly updated within the same batch
251245
existingChange.changedFields = {
252246
...existingChange.changedFields,
253247
...(recordData.cellValuesByFieldId || {}),
254248
}
255-
// Keep changeType as 'created' if it started as created
256249
} else {
257-
// New creation
258250
consolidatedChangesMap.set(recordId, {
259251
tableId: tableId,
260252
recordId: recordId,
@@ -265,7 +257,6 @@ async function fetchAndProcessAirtablePayloads(
265257
}
266258
}
267259

268-
// Handle updated records
269260
if (tableChanges.changedRecordsById) {
270261
const updatedCount = Object.keys(tableChanges.changedRecordsById).length
271262
changeCount += updatedCount
@@ -277,16 +268,12 @@ async function fetchAndProcessAirtablePayloads(
277268
const currentFields = recordData.current?.cellValuesByFieldId || {}
278269

279270
if (existingChange) {
280-
// Existing record was updated again
281271
existingChange.changedFields = {
282272
...existingChange.changedFields,
283273
...currentFields,
284274
}
285-
// Ensure type is 'updated' if it was previously 'created'
286275
existingChange.changeType = 'updated'
287-
// Do not update previousFields again
288276
} else {
289-
// First update for this record in the batch
290277
const newChange: AirtableChange = {
291278
tableId: tableId,
292279
recordId: recordId,
@@ -316,7 +303,6 @@ async function fetchAndProcessAirtablePayloads(
316303
externalWebhookCursor: currentCursor,
317304
}
318305
try {
319-
// Force a complete object update to ensure consistency in serverless env
320306
await db
321307
.update(webhook)
322308
.set({
@@ -325,7 +311,7 @@ async function fetchAndProcessAirtablePayloads(
325311
})
326312
.where(eq(webhook.id, webhookData.id as string))
327313

328-
localProviderConfig.externalWebhookCursor = currentCursor // Update local copy too
314+
localProviderConfig.externalWebhookCursor = currentCursor
329315
} catch (dbError: unknown) {
330316
const err = dbError as Error
331317
logger.error(`[${requestId}] Failed to persist Airtable cursor to DB`, {
@@ -334,7 +320,7 @@ async function fetchAndProcessAirtablePayloads(
334320
error: err.message,
335321
})
336322
mightHaveMore = false
337-
throw new Error('Failed to save Airtable cursor, stopping processing.') // Re-throw to break loop clearly
323+
throw new Error('Failed to save Airtable cursor, stopping processing.')
338324
}
339325
} else if (!nextCursor || typeof nextCursor !== 'number') {
340326
logger.warn(`[${requestId}] Invalid or missing cursor received, stopping poll`, {
@@ -344,7 +330,7 @@ async function fetchAndProcessAirtablePayloads(
344330
})
345331
mightHaveMore = false
346332
} else if (nextCursor === currentCursor) {
347-
mightHaveMore = false // Explicitly stop if cursor hasn't changed
333+
mightHaveMore = false
348334
}
349335
} catch (fetchError: unknown) {
350336
logger.error(
@@ -355,7 +341,6 @@ async function fetchAndProcessAirtablePayloads(
355341
break
356342
}
357343
}
358-
// Convert map values to array for final processing
359344
const finalConsolidatedChanges = Array.from(consolidatedChangesMap.values())
360345
logger.info(
361346
`[${requestId}] Consolidated ${finalConsolidatedChanges.length} Airtable changes across ${apiCallCount} API calls`
@@ -367,9 +352,7 @@ async function fetchAndProcessAirtablePayloads(
367352
const input: Record<string, unknown> = {
368353
payloads: allPayloads,
369354
latestPayload,
370-
// Consolidated, simplified changes for convenience
371355
airtableChanges: finalConsolidatedChanges,
372-
// Include webhook metadata for resolver fallbacks
373356
webhook: {
374357
data: {
375358
provider: 'airtable',
@@ -416,7 +399,6 @@ async function fetchAndProcessAirtablePayloads(
416399
})
417400
}
418401
} catch (error) {
419-
// Catch any unexpected errors during the setup/polling logic itself
420402
logger.error(
421403
`[${requestId}] Unexpected error during asynchronous Airtable payload processing task`,
422404
{

apps/sim/lib/webhooks/providers/hubspot.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,6 @@ export const hubspotHandler: WebhookProviderHandler = {
3030
}
3131

3232
try {
33-
/**
34-
* HubSpot v1 signature: SHA-256 of (clientSecret + requestBody), verified against X-HubSpot-Signature.
35-
* v1 is intentionally used for CRM webhook subscriptions — v3 requires the full request URL and method,
36-
* which are not available in verifyAuth.
37-
*/
3833
const computedHash = crypto
3934
.createHash('sha256')
4035
.update(clientSecret + rawBody, 'utf8')

apps/sim/lib/webhooks/providers/webflow.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { createLogger } from '@sim/logger'
33
import { safeCompare } from '@sim/security/compare'
44
import { toError } from '@sim/utils/errors'
55
import { NextResponse } from 'next/server'
6+
import { env } from '@/lib/core/config/env'
67
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
78
import { getBaseUrl } from '@/lib/core/utils/urls'
89
import { getCredentialOwner, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils'
@@ -25,11 +26,10 @@ export const webflowHandler: WebhookProviderHandler = {
2526
const secretKey = providerConfig.secretKey as string | undefined | null
2627

2728
if (!secretKey) {
28-
// Fail-open for existing webhooks created before this change (no secretKey stored)
2929
logger.warn(
30-
`[${requestId}] Webflow webhook missing secretKey in providerConfig — skipping signature verification`
30+
`[${requestId}] Webflow webhook missing secretKey in providerConfig — rejecting request`
3131
)
32-
return null
32+
return new NextResponse('Unauthorized - Webhook secret not configured', { status: 401 })
3333
}
3434

3535
const signature = request.headers.get('x-webflow-signature')
@@ -40,16 +40,14 @@ export const webflowHandler: WebhookProviderHandler = {
4040
return new NextResponse('Unauthorized - Missing Webflow signature headers', { status: 401 })
4141
}
4242

43-
// Replay protection: reject if timestamp is more than 5 minutes old.
44-
// x-webflow-timestamp is Unix milliseconds (e.g. 1722370035277) — compare directly with Date.now().
43+
// x-webflow-timestamp is Unix milliseconds — compare directly with Date.now()
4544
const ts = Number.parseInt(timestamp, 10)
4645
if (Number.isNaN(ts) || Date.now() - ts > 5 * 60 * 1000) {
4746
logger.warn(`[${requestId}] Webflow webhook timestamp expired or invalid`)
4847
return new NextResponse('Unauthorized - Webhook timestamp expired', { status: 401 })
4948
}
5049

5150
try {
52-
// HMAC-SHA256 of "${timestamp}:${rawBody}"
5351
const computedHash = crypto
5452
.createHmac('sha256', secretKey)
5553
.update(`${timestamp}:${rawBody}`, 'utf8')
@@ -188,11 +186,11 @@ export const webflowHandler: WebhookProviderHandler = {
188186
return {
189187
providerConfigUpdates: {
190188
externalId: responseBody.id || responseBody._id,
191-
secretKey: responseBody.secretKey,
189+
secretKey: responseBody.secretKey ?? env.WEBFLOW_CLIENT_SECRET ?? null,
192190
},
193191
}
194192
} catch (error: unknown) {
195-
const err = error as Error
193+
const err = toError(error)
196194
logger.error(
197195
`[${requestId}] Exception during Webflow webhook creation for webhook ${webhookRecord.id}.`,
198196
{

0 commit comments

Comments
 (0)