Skip to content

Commit 36c8db9

Browse files
committed
refactor(netlify): switch trigger to manual webhook setup
Removes auto-registration of Netlify outgoing webhooks because the data.signature_secret field doesn't reliably round-trip through the hooks API. The user now configures the webhook in Netlify's dashboard and pastes the JWS secret token into the trigger; Sim only verifies inbound signatures with that secret. Drops apiKey and siteId from the trigger config (still required on the block for tool operations).
1 parent d6b44f0 commit 36c8db9

2 files changed

Lines changed: 17 additions & 171 deletions

File tree

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

Lines changed: 3 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,11 @@ import crypto from 'crypto'
22
import { createLogger } from '@sim/logger'
33
import { safeCompare } from '@sim/security/compare'
44
import { NextResponse } from 'next/server'
5-
import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils'
65
import type {
76
AuthContext,
8-
DeleteSubscriptionContext,
97
EventMatchContext,
108
FormatInputContext,
119
FormatInputResult,
12-
SubscriptionContext,
13-
SubscriptionResult,
1410
WebhookProviderHandler,
1511
} from '@/lib/webhooks/providers/types'
1612

@@ -53,11 +49,11 @@ function verifyNetlifyJwt(token: string, secret: string, rawBody: string): boole
5349

5450
export const netlifyHandler: WebhookProviderHandler = {
5551
verifyAuth({ request, rawBody, requestId, providerConfig }: AuthContext): NextResponse | null {
56-
const secret = (providerConfig.webhookSecret as string | undefined)?.trim()
52+
const secret = (providerConfig.signatureSecret as string | undefined)?.trim()
5753
if (!secret) {
58-
logger.warn(`[${requestId}] Netlify webhook secret missing; rejecting delivery`)
54+
logger.warn(`[${requestId}] Netlify signature secret missing; rejecting delivery`)
5955
return new NextResponse(
60-
'Unauthorized - Netlify webhook signing secret is not configured. Re-save the trigger so a webhook can be registered.',
56+
'Unauthorized - Netlify signature secret is not configured. Set the JWS secret token on this trigger.',
6157
{ status: 401 }
6258
)
6359
}
@@ -105,152 +101,6 @@ export const netlifyHandler: WebhookProviderHandler = {
105101
return `netlify:${String(id)}`
106102
},
107103

108-
async createSubscription(ctx: SubscriptionContext): Promise<SubscriptionResult | undefined> {
109-
const { webhook, requestId } = ctx
110-
try {
111-
const providerConfig = getProviderConfig(webhook)
112-
const apiKey = providerConfig.apiKey as string | undefined
113-
const triggerId = providerConfig.triggerId as string | undefined
114-
const siteId = (providerConfig.siteId as string | undefined)?.trim()
115-
116-
if (!apiKey) {
117-
throw new Error(
118-
'Netlify Personal Access Token is required. Provide your access token in the trigger configuration.'
119-
)
120-
}
121-
if (!siteId) {
122-
throw new Error('Netlify Site ID is required to register a deploy webhook.')
123-
}
124-
if (!triggerId) {
125-
throw new Error('Missing trigger ID — re-save the Netlify trigger.')
126-
}
127-
128-
const { NETLIFY_TRIGGER_EVENT_TYPES } = await import('@/triggers/netlify/utils')
129-
const event = NETLIFY_TRIGGER_EVENT_TYPES[triggerId]
130-
if (!event) {
131-
throw new Error(
132-
`Unknown Netlify trigger "${triggerId}". Remove and re-add the Netlify trigger, then save again.`
133-
)
134-
}
135-
136-
const notificationUrl = getNotificationUrl(webhook)
137-
const signingSecret = crypto.randomBytes(32).toString('base64url')
138-
139-
logger.info(`[${requestId}] Creating Netlify webhook`, {
140-
triggerId,
141-
event,
142-
siteId,
143-
webhookId: webhook.id,
144-
})
145-
146-
const apiUrl = `https://api.netlify.com/api/v1/hooks?site_id=${encodeURIComponent(siteId)}`
147-
const requestBody = {
148-
type: 'url',
149-
event,
150-
data: {
151-
url: notificationUrl,
152-
signature_secret: signingSecret,
153-
},
154-
}
155-
156-
const netlifyResponse = await fetch(apiUrl, {
157-
method: 'POST',
158-
headers: {
159-
Authorization: `Bearer ${apiKey}`,
160-
'Content-Type': 'application/json',
161-
},
162-
body: JSON.stringify(requestBody),
163-
})
164-
165-
const responseBody = (await netlifyResponse.json().catch(() => ({}))) as Record<
166-
string,
167-
unknown
168-
>
169-
170-
if (!netlifyResponse.ok) {
171-
const errorMessage =
172-
(responseBody.message as string) ||
173-
(responseBody.error as string) ||
174-
'Unknown Netlify API error'
175-
176-
let userFriendlyMessage = 'Failed to create webhook subscription in Netlify'
177-
if (netlifyResponse.status === 401 || netlifyResponse.status === 403) {
178-
userFriendlyMessage =
179-
'Invalid or insufficient Netlify Personal Access Token. Verify the token has access to this site.'
180-
} else if (netlifyResponse.status === 404) {
181-
userFriendlyMessage = `Netlify site "${siteId}" not found or not accessible with this token.`
182-
} else if (errorMessage && errorMessage !== 'Unknown Netlify API error') {
183-
userFriendlyMessage = `Netlify error: ${errorMessage}`
184-
}
185-
186-
throw new Error(userFriendlyMessage)
187-
}
188-
189-
const externalId = (responseBody.id as string | undefined) ?? undefined
190-
if (!externalId) {
191-
throw new Error('Netlify webhook creation succeeded but no hook ID was returned')
192-
}
193-
194-
logger.info(`[${requestId}] Successfully created Netlify hook ${externalId}`, {
195-
webhookId: webhook.id,
196-
event,
197-
})
198-
199-
return {
200-
providerConfigUpdates: {
201-
externalId,
202-
webhookSecret: signingSecret,
203-
},
204-
}
205-
} catch (error: unknown) {
206-
const err = error as Error
207-
logger.error(`[${requestId}] Exception during Netlify webhook creation`, {
208-
message: err.message,
209-
webhookId: webhook.id,
210-
})
211-
throw error
212-
}
213-
},
214-
215-
async deleteSubscription(ctx: DeleteSubscriptionContext): Promise<void> {
216-
const { webhook, requestId } = ctx
217-
try {
218-
const config = getProviderConfig(webhook)
219-
const apiKey = config.apiKey as string | undefined
220-
const externalId = config.externalId as string | undefined
221-
222-
if (!apiKey || !externalId) {
223-
logger.warn(
224-
`[${requestId}] Missing apiKey or externalId for Netlify webhook deletion ${webhook.id}, skipping cleanup`
225-
)
226-
if (ctx.strict) throw new Error('Missing Netlify webhook deletion credentials')
227-
return
228-
}
229-
230-
const apiUrl = `https://api.netlify.com/api/v1/hooks/${encodeURIComponent(externalId)}`
231-
232-
const response = await fetch(apiUrl, {
233-
method: 'DELETE',
234-
headers: {
235-
Authorization: `Bearer ${apiKey}`,
236-
},
237-
})
238-
239-
if (!response.ok && response.status !== 404) {
240-
logger.warn(
241-
`[${requestId}] Failed to delete Netlify webhook (non-fatal): ${response.status}`
242-
)
243-
if (ctx.strict) throw new Error(`Failed to delete Netlify webhook: ${response.status}`)
244-
} else {
245-
await response.body?.cancel()
246-
logger.info(`[${requestId}] Successfully deleted Netlify hook ${externalId}`)
247-
}
248-
} catch (error) {
249-
logger.warn(`[${requestId}] Error deleting Netlify webhook (non-fatal)`, error)
250-
if (ctx.strict) throw error
251-
}
252-
},
253-
254104
async formatInput(ctx: FormatInputContext): Promise<FormatInputResult> {
255105
const body = ctx.body as Record<string, unknown>
256106

apps/sim/triggers/netlify/utils.ts

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,16 @@ export function isNetlifyEventMatch(triggerId: string, state: string | undefined
5454

5555
/**
5656
* Generates HTML setup instructions shown inside the trigger config panel.
57+
* Netlify outgoing webhooks are configured manually in the Netlify dashboard;
58+
* Sim verifies inbound deliveries using the JWS secret token the user provides.
5759
*/
5860
export function netlifySetupInstructions(eventLabel: string): string {
5961
const instructions = [
60-
'Generate a Personal Access Token at <strong>User settings → Applications → Personal access tokens</strong> (<a href="https://app.netlify.com/user/applications#personal-access-tokens" target="_blank" rel="noreferrer">direct link</a>) and paste it above.',
61-
'Enter the target <strong>Site ID</strong> (or primary domain) of the Netlify site to listen on.',
62-
`<strong>Deploy</strong> the workflow — Sim will automatically register an outgoing webhook in Netlify for <strong>${eventLabel}</strong> events on the chosen site.`,
63-
'The webhook is automatically removed from Netlify when you delete this trigger or undeploy the workflow.',
62+
'Copy the <strong>Webhook URL</strong> above.',
63+
'In Netlify open <strong>Site settings → Build &amp; deploy → Deploy notifications → Add notification → Outgoing webhook</strong>.',
64+
`Paste the URL, choose <strong>${eventLabel}</strong> as the event to listen for, generate a <strong>JWS secret token</strong>, and save the notification.`,
65+
'Paste that same JWS secret into the <strong>Signature Secret</strong> field above, then <strong>Deploy</strong> the workflow.',
66+
'Remove the notification in Netlify when you delete this trigger — Sim does not manage the webhook on your behalf.',
6467
]
6568

6669
return instructions
@@ -73,31 +76,24 @@ export function netlifySetupInstructions(eventLabel: string): string {
7376

7477
/**
7578
* Netlify-specific extra fields exposed in trigger configuration.
79+
* The signature secret is the JWS secret token the user configures on the
80+
* outgoing webhook in Netlify; we use it to verify inbound deliveries.
7681
*/
7782
export function buildNetlifyExtraFields(triggerId: string): SubBlockConfig[] {
7883
return [
7984
{
80-
id: 'apiKey',
81-
title: 'Access Token',
85+
id: 'signatureSecret',
86+
title: 'Signature Secret',
8287
type: 'short-input' as const,
83-
placeholder: 'Enter your Netlify Personal Access Token',
84-
description: 'Required to register and remove the webhook in Netlify.',
88+
placeholder: 'Paste the JWS secret token configured in Netlify',
89+
description:
90+
'The JWS secret token set on the outgoing webhook in Netlify. Used to verify the signature of every incoming delivery.',
8591
password: true,
8692
required: true,
8793
paramVisibility: 'user-only',
8894
mode: 'trigger' as const,
8995
condition: { field: 'selectedTriggerId', value: triggerId },
9096
},
91-
{
92-
id: 'siteId',
93-
title: 'Site ID',
94-
type: 'short-input' as const,
95-
placeholder: 'Site ID or primary domain (e.g., 0d3a9d2f-... or my-site.netlify.app)',
96-
description: 'The Netlify site whose deploys will trigger this workflow.',
97-
required: true,
98-
mode: 'trigger' as const,
99-
condition: { field: 'selectedTriggerId', value: triggerId },
100-
},
10197
]
10298
}
10399

0 commit comments

Comments
 (0)