feat(netlify): add Netlify block with deploys, env vars, and deploy triggers#4518
feat(netlify): add Netlify block with deploys, env vars, and deploy triggers#4518stylessh wants to merge 5 commits intosimstudioai:stagingfrom
Conversation
…riggers Adds the Netlify integration with nine tool operations (list sites, list/get/cancel/create deploys, and list/create/update/delete env vars) plus six webhook triggers (deploy created, building, succeeded, failed, locked, unlocked). Tools authenticate via a Netlify Personal Access Token (Bearer). Triggers automatically register an outgoing webhook on the chosen site at deploy time and clean it up on undeploy. Inbound webhook signatures are verified as JWTs (HS256, iss=netlify, sha256 claim of the raw body) without adding a jsonwebtoken dependency.
|
@stylessh is attempting to deploy a commit to the Sim Team on Vercel. A member of the Team first needs to authorize it. |
PR SummaryMedium Risk Overview Introduces 6 deploy-related webhook triggers and a new Reviewed by Cursor Bugbot for commit a5a8100. Bugbot is set up for automated code reviews on this repo. Configure here. |
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).
Greptile SummaryThis PR adds a Netlify integration with nine tool operations (sites, deploys, env vars) and six deploy webhook triggers (
Confidence Score: 4/5Safe to merge with one recommended follow-up: add JWT exp claim validation before this goes to production webhook traffic. The integration is well-structured and follows existing patterns throughout. The webhook JWT verification correctly handles HMAC-SHA256, issuer, and body-hash checks, but omits the exp claim — Netlify JWTs are short-lived and include exp, so a captured token remains indefinitely replayable without that guard. The idempotency check on deploy ID reduces the practical impact, but it is not a complete substitute. Everything else — tool params, env var CRUD, hook registration/deletion, event routing — looks correct. apps/sim/lib/webhooks/providers/netlify.ts — the JWT verification function needs an exp claim check before reaching production webhook traffic.
|
| Filename | Overview |
|---|---|
| apps/sim/lib/webhooks/providers/netlify.ts | Core webhook provider: JWT verification is solid (HMAC-SHA256, iss check, body-hash) but missing exp claim validation; subscription/deletion logic is well-structured. |
| apps/sim/triggers/netlify/utils.ts | Event matching, setup instructions, and output schema helpers; state-based filter for deploy_created/locked/unlocked falls to a permissive default that is safe in practice but lacks explicit guards. |
| apps/sim/blocks/blocks/netlify.ts | Block definition with nine operations, trigger integration, and conditional field display; correctly structured following existing block patterns. |
| apps/sim/tools/netlify/get_deploy.ts | Get deploy tool; NetlifyApiDeploy interface is duplicated verbatim in cancel_deploy.ts and list_deploys.ts — should be extracted to a shared location. |
| apps/sim/tools/netlify/create_deploy.ts | Triggers a build via POST /sites/{id}/builds with branch/title/clear_cache as query params; correct Netlify API usage. |
| apps/sim/tools/netlify/create_env_var.ts | Correctly wraps the env var body in an array as required by Netlify's POST /accounts/{id}/env endpoint. |
| apps/sim/tools/netlify/delete_env_var.ts | Delete env var tool; transformResponse unconditionally returns deleted:true, which is correct since the framework handles non-2xx errors upstream and DELETE 204 carries no body. |
| apps/sim/tools/netlify/utils.ts | Shared normalizeEnvVar and buildEnvBody helpers used by create/update env var tools; clean and correct. |
Sequence Diagram
sequenceDiagram
participant User
participant SimBlock as Sim Netlify Block
participant NetlifyAPI as Netlify API
participant SimWebhook as Sim Webhook Endpoint
participant Workflow as Sim Workflow
Note over User,Workflow: Tool Operations (e.g. Create Deploy)
User->>SimBlock: Configure operation + PAT
SimBlock->>NetlifyAPI: "POST /sites/{id}/builds?branch=..."
NetlifyAPI-->>SimBlock: Build object (id, deploy_id, ...)
SimBlock-->>User: Normalised output
Note over User,Workflow: Trigger Registration (Deploy)
User->>SimBlock: Deploy workflow with Netlify trigger
SimBlock->>NetlifyAPI: "POST /hooks?site_id=... with event + signature_secret"
NetlifyAPI-->>SimBlock: Hook ID
SimBlock->>SimBlock: Store externalId + webhookSecret
Note over User,Workflow: Inbound Webhook
NetlifyAPI->>SimWebhook: POST deploy payload + X-Webhook-Signature JWT
SimWebhook->>SimWebhook: verifyNetlifyJwt (sig + body-hash, no exp check)
SimWebhook->>SimWebhook: matchEvent — triggerId + state
SimWebhook->>Workflow: formatInput and run workflow
Workflow-->>SimWebhook: 200 OK
Note over User,Workflow: Trigger Deletion (Undeploy)
User->>SimBlock: Undeploy workflow
SimBlock->>NetlifyAPI: "DELETE /hooks/{externalId}"
NetlifyAPI-->>SimBlock: 204 No Content
Reviews (1): Last reviewed commit: "refactor(netlify): switch trigger to man..." | Re-trigger Greptile
|
|
||
| const expectedSignature = crypto | ||
| .createHmac('sha256', secret) | ||
| .update(`${headerB64}.${payloadB64}`) | ||
| .digest('base64url') | ||
|
|
||
| if (!safeCompare(expectedSignature, signatureB64)) { | ||
| return false | ||
| } | ||
|
|
||
| let payload: Record<string, unknown> | ||
| try { | ||
| payload = JSON.parse(Buffer.from(payloadB64, 'base64url').toString('utf8')) as Record< | ||
| string, | ||
| unknown | ||
| > | ||
| } catch { | ||
| return false | ||
| } | ||
|
|
||
| if (payload.iss !== 'netlify') return false | ||
|
|
||
| const bodyHash = crypto.createHash('sha256').update(rawBody, 'utf8').digest('hex') | ||
| if (typeof payload.sha256 !== 'string') return false | ||
| return safeCompare(payload.sha256, bodyHash) | ||
| } | ||
|
|
||
| export const netlifyHandler: WebhookProviderHandler = { | ||
| verifyAuth({ request, rawBody, requestId, providerConfig }: AuthContext): NextResponse | null { | ||
| const secret = (providerConfig.signatureSecret as string | undefined)?.trim() |
There was a problem hiding this comment.
Missing JWT
exp claim validation
verifyNetlifyJwt validates the HMAC signature and the sha256 body-hash but never checks the exp claim. Netlify JWTs are short-lived (typically valid for ~30 seconds after delivery). Without an expiry check, a previously-captured valid token can be replayed indefinitely — the signature and body-hash will still pass because neither changes between replays. The extractIdempotencyId guard limits duplicate workflow executions for the same deploy ID, but a replay with the same JWT + body would bypass signature freshness entirely.
| interface NetlifyApiDeploy { | ||
| id?: string | ||
| site_id?: string | ||
| state?: string | ||
| name?: string | ||
| url?: string | ||
| deploy_url?: string | ||
| deploy_ssl_url?: string | ||
| admin_url?: string | ||
| branch?: string | ||
| context?: string | ||
| commit_ref?: string | ||
| commit_url?: string | ||
| error_message?: string | ||
| created_at?: string | ||
| updated_at?: string | ||
| published_at?: string | ||
| } |
There was a problem hiding this comment.
Duplicated
NetlifyApiDeploy interface
The same NetlifyApiDeploy interface is defined independently in get_deploy.ts, cancel_deploy.ts, and list_deploys.ts. Moving this to types.ts (or utils.ts) would eliminate the duplication and make future field additions consistent across all three files.
| interface NetlifyApiDeploy { | |
| id?: string | |
| site_id?: string | |
| state?: string | |
| name?: string | |
| url?: string | |
| deploy_url?: string | |
| deploy_ssl_url?: string | |
| admin_url?: string | |
| branch?: string | |
| context?: string | |
| commit_ref?: string | |
| commit_url?: string | |
| error_message?: string | |
| created_at?: string | |
| updated_at?: string | |
| published_at?: string | |
| } | |
| // Consider extracting this interface to @/tools/netlify/types.ts or @/tools/netlify/utils.ts | |
| // to avoid the identical definition in get_deploy.ts, cancel_deploy.ts, and list_deploys.ts. | |
| interface NetlifyApiDeploy { | |
| id?: string | |
| site_id?: string | |
| state?: string | |
| name?: string | |
| url?: string | |
| deploy_url?: string | |
| deploy_ssl_url?: string | |
| admin_url?: string | |
| branch?: string | |
| context?: string | |
| commit_ref?: string | |
| commit_url?: string | |
| error_message?: string | |
| created_at?: string | |
| updated_at?: string | |
| published_at?: string | |
| } |
| export function isNetlifyEventMatch(triggerId: string, state: string | undefined): boolean { | ||
| const expected = NETLIFY_TRIGGER_EVENT_TYPES[triggerId] | ||
| if (!expected) { | ||
| return false | ||
| } | ||
| if (!state) { | ||
| return true | ||
| } | ||
| switch (expected) { | ||
| case 'deploy_succeeded': | ||
| return state === 'ready' | ||
| case 'deploy_failed': | ||
| return state === 'error' || state === 'rejected' | ||
| case 'deploy_building': | ||
| return state === 'building' | ||
| default: | ||
| return true | ||
| } |
There was a problem hiding this comment.
isNetlifyEventMatch returns true for all states on deploy_created, deploy_locked, and deploy_unlocked
The default: return true branch allows any state value to pass for the three unlisted event types. In practice this is safe because Netlify fires each event-specific hook in isolation. However, if two triggers for the same site share a notification endpoint (or if a future refactor coalesces URLs), a payload with an unexpected state would still match. Adding explicit state guards for deploy_created (enqueued | new), deploy_locked, and deploy_unlocked would make the check fully authoritative.
| 'Content-Type': 'application/json', | ||
| }), | ||
| body: () => ({}), | ||
| }, |
There was a problem hiding this comment.
Create deploy sends branch in query params instead of body
High Severity
The create_deploy tool passes branch, title, and clear_cache as URL query parameters while sending an empty body (body: () => ({})). The Netlify POST /api/v1/sites/{site_id}/builds endpoint expects these parameters in the JSON request body (e.g. {"branch": "main"}). This means the branch selection, title, and cache-clearing options are silently ignored, and every build triggers on the default production branch regardless of user input.
Reviewed by Cursor Bugbot for commit 36c8db9. Configure here.
| created_at?: string | ||
| updated_at?: string | ||
| published_at?: string | ||
| } |
There was a problem hiding this comment.
Duplicated NetlifyApiDeploy interface across three tool files
Low Severity
The identical NetlifyApiDeploy interface (18 lines) is copy-pasted across cancel_deploy.ts, get_deploy.ts, and list_deploys.ts. This triples the maintenance surface — a field change or addition needs updating in all three files. It naturally belongs in utils.ts alongside the already-shared NetlifyApiEnvVar interface.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 36c8db9. Configure here.
Multiple Netlify deploy events (created, building, succeeded) share the same deploy id, so keying dedup only on id collapsed distinct events into one. Including state plus the locked flag differentiates each event for the same deploy.
Runs generate-docs.ts to produce the tool and trigger MDX pages, the integrations.json entry, and icon mappings for the docs app. Adds Netlify to the trigger provider display-name map so the docs render the proper label. Reorders the webhook provider registry so netlify sits alphabetically between monday and notion instead of being parked next to vercel.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 287fbb1. Configure here.
| default: | ||
| return base | ||
| } | ||
| }, |
There was a problem hiding this comment.
Deploy siteId leaks into env var operations via ...rest
Medium Severity
The siteId sub-block (used for deploy operations) is not destructured from params, so it flows into ...rest and then into base. For env var operations like list_env_vars, create_env_var, update_env_var, and delete_env_var, envSiteId is conditionally spread as siteId only when truthy. When envSiteId is empty (it's optional), the deploy-scoped siteId already present in base leaks through and is sent to the env var tool, which uses it to scope the query to that site — silently filtering results to an unintended site.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 287fbb1. Configure here.
The list_sites and list_deploys tools support page and per_page parameters but the block didn't surface them, so users couldn't paginate from the UI. Adds Limit and Page advanced fields with numeric coercion in tools.config.params. Expands the block outputs for get_deploy, cancel_deploy, and create_deploy so the tag dropdown surfaces siteId, branch, commitRef, errorMessage, sha, and done in addition to id and state. The tools already returned these, but only a subset was discoverable.


Summary
Authorization: Bearer <token>.deploy_created,deploy_building,deploy_succeeded,deploy_failed,deploy_locked,deploy_unlocked). Sim auto-registers an outgoing webhook on the chosen site at deploy time and removes it on undeploy.iss=netlify, with thesha256claim matchingsha256(rawBody)) using thecryptostandard library — nojsonwebtokendependency added.Notes on Netlify specifics
POST /sites/{site_id}/builds?branch=...), which produces a deploy.data.signature_secretwhen creating the hook. If Netlify ever changes that field, the signature check is the canary.Test plan
nfp_PATbranch)siteIdfor site override)create_deploy) and verify the workflow fires with a normalized deploy payloadX-Webhook-SignatureJWT is verified (tamper with the body locally to confirm a 401)