mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
feat(triggers): add Resend webhook triggers with auto-registration (#3986)
* feat(triggers): add Resend webhook triggers with auto-registration * fix(triggers): capture Resend signing secret and add Svix webhook verification * fix(triggers): add paramVisibility, event-type filtering for Resend triggers * fix(triggers): add Svix timestamp staleness check to prevent replay attacks Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(triggers): use Number.parseInt and Number.isNaN for lint compliance Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { ResendIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
import { getTrigger } from '@/triggers'
|
||||
|
||||
export const ResendBlock: BlockConfig = {
|
||||
type: 'resend',
|
||||
@@ -16,6 +17,20 @@ export const ResendBlock: BlockConfig = {
|
||||
icon: ResendIcon,
|
||||
authMode: AuthMode.ApiKey,
|
||||
|
||||
triggers: {
|
||||
enabled: true,
|
||||
available: [
|
||||
'resend_email_sent',
|
||||
'resend_email_delivered',
|
||||
'resend_email_bounced',
|
||||
'resend_email_complained',
|
||||
'resend_email_opened',
|
||||
'resend_email_clicked',
|
||||
'resend_email_failed',
|
||||
'resend_webhook',
|
||||
],
|
||||
},
|
||||
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'operation',
|
||||
@@ -221,6 +236,15 @@ Return ONLY the email body - no explanations, no extra text.`,
|
||||
condition: { field: 'operation', value: ['get_contact', 'update_contact', 'delete_contact'] },
|
||||
required: true,
|
||||
},
|
||||
|
||||
...getTrigger('resend_email_sent').subBlocks,
|
||||
...getTrigger('resend_email_delivered').subBlocks,
|
||||
...getTrigger('resend_email_bounced').subBlocks,
|
||||
...getTrigger('resend_email_complained').subBlocks,
|
||||
...getTrigger('resend_email_opened').subBlocks,
|
||||
...getTrigger('resend_email_clicked').subBlocks,
|
||||
...getTrigger('resend_email_failed').subBlocks,
|
||||
...getTrigger('resend_webhook').subBlocks,
|
||||
],
|
||||
|
||||
tools: {
|
||||
|
||||
@@ -23,6 +23,8 @@ const SYSTEM_MANAGED_FIELDS = new Set([
|
||||
'eventTypes',
|
||||
'webhookTag',
|
||||
'webhookSecret',
|
||||
'signingSecret',
|
||||
'secretToken',
|
||||
'historyId',
|
||||
'lastCheckedTimestamp',
|
||||
'setupCompleted',
|
||||
|
||||
@@ -21,6 +21,7 @@ import { lemlistHandler } from '@/lib/webhooks/providers/lemlist'
|
||||
import { linearHandler } from '@/lib/webhooks/providers/linear'
|
||||
import { microsoftTeamsHandler } from '@/lib/webhooks/providers/microsoft-teams'
|
||||
import { outlookHandler } from '@/lib/webhooks/providers/outlook'
|
||||
import { resendHandler } from '@/lib/webhooks/providers/resend'
|
||||
import { rssHandler } from '@/lib/webhooks/providers/rss'
|
||||
import { slackHandler } from '@/lib/webhooks/providers/slack'
|
||||
import { stripeHandler } from '@/lib/webhooks/providers/stripe'
|
||||
@@ -55,6 +56,7 @@ const PROVIDER_HANDLERS: Record<string, WebhookProviderHandler> = {
|
||||
jira: jiraHandler,
|
||||
lemlist: lemlistHandler,
|
||||
linear: linearHandler,
|
||||
resend: resendHandler,
|
||||
'microsoft-teams': microsoftTeamsHandler,
|
||||
outlook: outlookHandler,
|
||||
rss: rssHandler,
|
||||
|
||||
294
apps/sim/lib/webhooks/providers/resend.ts
Normal file
294
apps/sim/lib/webhooks/providers/resend.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
import crypto from 'node:crypto'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { safeCompare } from '@/lib/core/security/encryption'
|
||||
import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/providers/subscription-utils'
|
||||
import type {
|
||||
AuthContext,
|
||||
DeleteSubscriptionContext,
|
||||
EventMatchContext,
|
||||
FormatInputContext,
|
||||
FormatInputResult,
|
||||
SubscriptionContext,
|
||||
SubscriptionResult,
|
||||
WebhookProviderHandler,
|
||||
} from '@/lib/webhooks/providers/types'
|
||||
|
||||
const logger = createLogger('WebhookProvider:Resend')
|
||||
|
||||
const ALL_RESEND_EVENTS = [
|
||||
'email.sent',
|
||||
'email.delivered',
|
||||
'email.delivery_delayed',
|
||||
'email.bounced',
|
||||
'email.complained',
|
||||
'email.opened',
|
||||
'email.clicked',
|
||||
'email.failed',
|
||||
'email.received',
|
||||
'email.scheduled',
|
||||
'email.suppressed',
|
||||
'contact.created',
|
||||
'contact.updated',
|
||||
'contact.deleted',
|
||||
'domain.created',
|
||||
'domain.updated',
|
||||
'domain.deleted',
|
||||
]
|
||||
|
||||
/**
|
||||
* Verify a Resend webhook signature using the Svix signing scheme.
|
||||
* Resend uses Svix under the hood: HMAC-SHA256 of `${svix-id}.${svix-timestamp}.${body}`
|
||||
* signed with the base64-decoded `whsec_...` secret.
|
||||
*/
|
||||
function verifySvixSignature(
|
||||
secret: string,
|
||||
msgId: string,
|
||||
timestamp: string,
|
||||
signatures: string,
|
||||
rawBody: string
|
||||
): boolean {
|
||||
try {
|
||||
const ts = Number.parseInt(timestamp, 10)
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
if (Number.isNaN(ts) || Math.abs(now - ts) > 5 * 60) {
|
||||
return false
|
||||
}
|
||||
|
||||
const secretBytes = Buffer.from(secret.replace(/^whsec_/, ''), 'base64')
|
||||
const toSign = `${msgId}.${timestamp}.${rawBody}`
|
||||
const expectedSignature = crypto
|
||||
.createHmac('sha256', secretBytes)
|
||||
.update(toSign, 'utf8')
|
||||
.digest('base64')
|
||||
|
||||
const providedSignatures = signatures.split(' ')
|
||||
for (const versionedSig of providedSignatures) {
|
||||
const parts = versionedSig.split(',')
|
||||
if (parts.length !== 2) continue
|
||||
const sig = parts[1]
|
||||
if (safeCompare(sig, expectedSignature)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
} catch (error) {
|
||||
logger.error('Error verifying Resend Svix signature:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const resendHandler: WebhookProviderHandler = {
|
||||
async verifyAuth({
|
||||
request,
|
||||
rawBody,
|
||||
requestId,
|
||||
providerConfig,
|
||||
}: AuthContext): Promise<NextResponse | null> {
|
||||
const signingSecret = providerConfig.signingSecret as string | undefined
|
||||
if (!signingSecret) {
|
||||
return null
|
||||
}
|
||||
|
||||
const svixId = request.headers.get('svix-id')
|
||||
const svixTimestamp = request.headers.get('svix-timestamp')
|
||||
const svixSignature = request.headers.get('svix-signature')
|
||||
|
||||
if (!svixId || !svixTimestamp || !svixSignature) {
|
||||
logger.warn(`[${requestId}] Resend webhook missing Svix signature headers`)
|
||||
return new NextResponse('Unauthorized - Missing Resend signature headers', { status: 401 })
|
||||
}
|
||||
|
||||
if (!verifySvixSignature(signingSecret, svixId, svixTimestamp, svixSignature, rawBody)) {
|
||||
logger.warn(`[${requestId}] Resend Svix signature verification failed`)
|
||||
return new NextResponse('Unauthorized - Invalid Resend signature', { status: 401 })
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
|
||||
matchEvent({ body, providerConfig, requestId }: EventMatchContext): boolean {
|
||||
const triggerId = providerConfig.triggerId as string | undefined
|
||||
if (!triggerId || triggerId === 'resend_webhook') {
|
||||
return true
|
||||
}
|
||||
|
||||
const EVENT_TYPE_MAP: Record<string, string> = {
|
||||
resend_email_sent: 'email.sent',
|
||||
resend_email_delivered: 'email.delivered',
|
||||
resend_email_bounced: 'email.bounced',
|
||||
resend_email_complained: 'email.complained',
|
||||
resend_email_opened: 'email.opened',
|
||||
resend_email_clicked: 'email.clicked',
|
||||
resend_email_failed: 'email.failed',
|
||||
}
|
||||
|
||||
const expectedType = EVENT_TYPE_MAP[triggerId]
|
||||
const actualType = (body as Record<string, unknown>)?.type as string | undefined
|
||||
|
||||
if (expectedType && actualType !== expectedType) {
|
||||
logger.debug(
|
||||
`[${requestId}] Resend event type mismatch: expected ${expectedType}, got ${actualType}. Skipping.`
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
|
||||
async formatInput({ body }: FormatInputContext): Promise<FormatInputResult> {
|
||||
const payload = body as Record<string, unknown>
|
||||
const data = payload.data as Record<string, unknown> | undefined
|
||||
const bounce = data?.bounce as Record<string, unknown> | undefined
|
||||
const click = data?.click as Record<string, unknown> | undefined
|
||||
|
||||
return {
|
||||
input: {
|
||||
type: payload.type,
|
||||
created_at: payload.created_at,
|
||||
email_id: data?.email_id ?? null,
|
||||
from: data?.from ?? null,
|
||||
to: data?.to ?? null,
|
||||
subject: data?.subject ?? null,
|
||||
bounceType: bounce?.type ?? null,
|
||||
bounceSubType: bounce?.subType ?? null,
|
||||
bounceMessage: bounce?.message ?? null,
|
||||
clickIpAddress: click?.ipAddress ?? null,
|
||||
clickLink: click?.link ?? null,
|
||||
clickTimestamp: click?.timestamp ?? null,
|
||||
clickUserAgent: click?.userAgent ?? null,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
async createSubscription(ctx: SubscriptionContext): Promise<SubscriptionResult | undefined> {
|
||||
const { webhook, requestId } = ctx
|
||||
try {
|
||||
const providerConfig = getProviderConfig(webhook)
|
||||
const apiKey = providerConfig.apiKey as string | undefined
|
||||
const triggerId = providerConfig.triggerId as string | undefined
|
||||
|
||||
if (!apiKey) {
|
||||
logger.warn(`[${requestId}] Missing apiKey for Resend webhook creation.`, {
|
||||
webhookId: webhook.id,
|
||||
})
|
||||
throw new Error(
|
||||
'Resend API Key is required. Please provide your Resend API Key in the trigger configuration.'
|
||||
)
|
||||
}
|
||||
|
||||
const eventTypeMap: Record<string, string[]> = {
|
||||
resend_email_sent: ['email.sent'],
|
||||
resend_email_delivered: ['email.delivered'],
|
||||
resend_email_bounced: ['email.bounced'],
|
||||
resend_email_complained: ['email.complained'],
|
||||
resend_email_opened: ['email.opened'],
|
||||
resend_email_clicked: ['email.clicked'],
|
||||
resend_email_failed: ['email.failed'],
|
||||
resend_webhook: ALL_RESEND_EVENTS,
|
||||
}
|
||||
|
||||
const events = eventTypeMap[triggerId ?? ''] ?? ALL_RESEND_EVENTS
|
||||
const notificationUrl = getNotificationUrl(webhook)
|
||||
|
||||
logger.info(`[${requestId}] Creating Resend webhook`, {
|
||||
triggerId,
|
||||
events,
|
||||
webhookId: webhook.id,
|
||||
})
|
||||
|
||||
const resendResponse = await fetch('https://api.resend.com/webhooks', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
endpoint: notificationUrl,
|
||||
events,
|
||||
}),
|
||||
})
|
||||
|
||||
const responseBody = (await resendResponse.json()) as Record<string, unknown>
|
||||
|
||||
if (!resendResponse.ok) {
|
||||
const errorMessage =
|
||||
(responseBody.message as string) ||
|
||||
(responseBody.name as string) ||
|
||||
'Unknown Resend API error'
|
||||
logger.error(
|
||||
`[${requestId}] Failed to create webhook in Resend for webhook ${webhook.id}. Status: ${resendResponse.status}`,
|
||||
{ message: errorMessage, response: responseBody }
|
||||
)
|
||||
|
||||
let userFriendlyMessage = 'Failed to create webhook subscription in Resend'
|
||||
if (resendResponse.status === 401 || resendResponse.status === 403) {
|
||||
userFriendlyMessage = 'Invalid Resend API Key. Please verify your API Key is correct.'
|
||||
} else if (errorMessage && errorMessage !== 'Unknown Resend API error') {
|
||||
userFriendlyMessage = `Resend error: ${errorMessage}`
|
||||
}
|
||||
|
||||
throw new Error(userFriendlyMessage)
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Successfully created webhook in Resend for webhook ${webhook.id}.`,
|
||||
{
|
||||
resendWebhookId: responseBody.id,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
providerConfigUpdates: {
|
||||
externalId: responseBody.id,
|
||||
signingSecret: responseBody.signing_secret,
|
||||
},
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error
|
||||
logger.error(
|
||||
`[${requestId}] Exception during Resend webhook creation for webhook ${webhook.id}.`,
|
||||
{
|
||||
message: err.message,
|
||||
stack: err.stack,
|
||||
}
|
||||
)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
|
||||
async deleteSubscription(ctx: DeleteSubscriptionContext): Promise<void> {
|
||||
const { webhook, requestId } = ctx
|
||||
try {
|
||||
const config = getProviderConfig(webhook)
|
||||
const apiKey = config.apiKey as string | undefined
|
||||
const externalId = config.externalId as string | undefined
|
||||
|
||||
if (!apiKey || !externalId) {
|
||||
logger.warn(
|
||||
`[${requestId}] Missing apiKey or externalId for Resend webhook deletion ${webhook.id}, skipping cleanup`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const resendResponse = await fetch(`https://api.resend.com/webhooks/${externalId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!resendResponse.ok && resendResponse.status !== 404) {
|
||||
const responseBody = await resendResponse.json().catch(() => ({}))
|
||||
logger.warn(
|
||||
`[${requestId}] Failed to delete Resend webhook (non-fatal): ${resendResponse.status}`,
|
||||
{ response: responseBody }
|
||||
)
|
||||
} else {
|
||||
logger.info(`[${requestId}] Successfully deleted Resend webhook ${externalId}`)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`[${requestId}] Error deleting Resend webhook (non-fatal)`, error)
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -171,6 +171,16 @@ import {
|
||||
microsoftTeamsWebhookTrigger,
|
||||
} from '@/triggers/microsoftteams'
|
||||
import { outlookPollingTrigger } from '@/triggers/outlook'
|
||||
import {
|
||||
resendEmailBouncedTrigger,
|
||||
resendEmailClickedTrigger,
|
||||
resendEmailComplainedTrigger,
|
||||
resendEmailDeliveredTrigger,
|
||||
resendEmailFailedTrigger,
|
||||
resendEmailOpenedTrigger,
|
||||
resendEmailSentTrigger,
|
||||
resendWebhookTrigger,
|
||||
} from '@/triggers/resend'
|
||||
import { rssPollingTrigger } from '@/triggers/rss'
|
||||
import {
|
||||
salesforceCaseStatusChangedTrigger,
|
||||
@@ -315,6 +325,14 @@ export const TRIGGER_REGISTRY: TriggerRegistry = {
|
||||
microsoftteams_webhook: microsoftTeamsWebhookTrigger,
|
||||
microsoftteams_chat_subscription: microsoftTeamsChatSubscriptionTrigger,
|
||||
outlook_poller: outlookPollingTrigger,
|
||||
resend_email_sent: resendEmailSentTrigger,
|
||||
resend_email_delivered: resendEmailDeliveredTrigger,
|
||||
resend_email_bounced: resendEmailBouncedTrigger,
|
||||
resend_email_complained: resendEmailComplainedTrigger,
|
||||
resend_email_opened: resendEmailOpenedTrigger,
|
||||
resend_email_clicked: resendEmailClickedTrigger,
|
||||
resend_email_failed: resendEmailFailedTrigger,
|
||||
resend_webhook: resendWebhookTrigger,
|
||||
rss_poller: rssPollingTrigger,
|
||||
salesforce_record_created: salesforceRecordCreatedTrigger,
|
||||
salesforce_record_updated: salesforceRecordUpdatedTrigger,
|
||||
|
||||
38
apps/sim/triggers/resend/email_bounced.ts
Normal file
38
apps/sim/triggers/resend/email_bounced.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { ResendIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
buildEmailBouncedOutputs,
|
||||
buildResendExtraFields,
|
||||
resendSetupInstructions,
|
||||
resendTriggerOptions,
|
||||
} from '@/triggers/resend/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
/**
|
||||
* Resend Email Bounced Trigger
|
||||
* Triggers when an email permanently bounces.
|
||||
*/
|
||||
export const resendEmailBouncedTrigger: TriggerConfig = {
|
||||
id: 'resend_email_bounced',
|
||||
name: 'Resend Email Bounced',
|
||||
provider: 'resend',
|
||||
description: 'Trigger workflow when an email bounces',
|
||||
version: '1.0.0',
|
||||
icon: ResendIcon,
|
||||
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'resend_email_bounced',
|
||||
triggerOptions: resendTriggerOptions,
|
||||
setupInstructions: resendSetupInstructions('email.bounced'),
|
||||
extraFields: buildResendExtraFields('resend_email_bounced'),
|
||||
}),
|
||||
|
||||
outputs: buildEmailBouncedOutputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
}
|
||||
38
apps/sim/triggers/resend/email_clicked.ts
Normal file
38
apps/sim/triggers/resend/email_clicked.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { ResendIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
buildEmailClickedOutputs,
|
||||
buildResendExtraFields,
|
||||
resendSetupInstructions,
|
||||
resendTriggerOptions,
|
||||
} from '@/triggers/resend/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
/**
|
||||
* Resend Email Clicked Trigger
|
||||
* Triggers when a recipient clicks a link in an email.
|
||||
*/
|
||||
export const resendEmailClickedTrigger: TriggerConfig = {
|
||||
id: 'resend_email_clicked',
|
||||
name: 'Resend Email Clicked',
|
||||
provider: 'resend',
|
||||
description: 'Trigger workflow when a link in an email is clicked',
|
||||
version: '1.0.0',
|
||||
icon: ResendIcon,
|
||||
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'resend_email_clicked',
|
||||
triggerOptions: resendTriggerOptions,
|
||||
setupInstructions: resendSetupInstructions('email.clicked'),
|
||||
extraFields: buildResendExtraFields('resend_email_clicked'),
|
||||
}),
|
||||
|
||||
outputs: buildEmailClickedOutputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
}
|
||||
38
apps/sim/triggers/resend/email_complained.ts
Normal file
38
apps/sim/triggers/resend/email_complained.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { ResendIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
buildEmailComplainedOutputs,
|
||||
buildResendExtraFields,
|
||||
resendSetupInstructions,
|
||||
resendTriggerOptions,
|
||||
} from '@/triggers/resend/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
/**
|
||||
* Resend Email Complained Trigger
|
||||
* Triggers when a recipient marks an email as spam.
|
||||
*/
|
||||
export const resendEmailComplainedTrigger: TriggerConfig = {
|
||||
id: 'resend_email_complained',
|
||||
name: 'Resend Email Complained',
|
||||
provider: 'resend',
|
||||
description: 'Trigger workflow when an email is marked as spam',
|
||||
version: '1.0.0',
|
||||
icon: ResendIcon,
|
||||
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'resend_email_complained',
|
||||
triggerOptions: resendTriggerOptions,
|
||||
setupInstructions: resendSetupInstructions('email.complained'),
|
||||
extraFields: buildResendExtraFields('resend_email_complained'),
|
||||
}),
|
||||
|
||||
outputs: buildEmailComplainedOutputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
}
|
||||
38
apps/sim/triggers/resend/email_delivered.ts
Normal file
38
apps/sim/triggers/resend/email_delivered.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { ResendIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
buildEmailDeliveredOutputs,
|
||||
buildResendExtraFields,
|
||||
resendSetupInstructions,
|
||||
resendTriggerOptions,
|
||||
} from '@/triggers/resend/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
/**
|
||||
* Resend Email Delivered Trigger
|
||||
* Triggers when an email is successfully delivered to the recipient's mail server.
|
||||
*/
|
||||
export const resendEmailDeliveredTrigger: TriggerConfig = {
|
||||
id: 'resend_email_delivered',
|
||||
name: 'Resend Email Delivered',
|
||||
provider: 'resend',
|
||||
description: 'Trigger workflow when an email is delivered',
|
||||
version: '1.0.0',
|
||||
icon: ResendIcon,
|
||||
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'resend_email_delivered',
|
||||
triggerOptions: resendTriggerOptions,
|
||||
setupInstructions: resendSetupInstructions('email.delivered'),
|
||||
extraFields: buildResendExtraFields('resend_email_delivered'),
|
||||
}),
|
||||
|
||||
outputs: buildEmailDeliveredOutputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
}
|
||||
38
apps/sim/triggers/resend/email_failed.ts
Normal file
38
apps/sim/triggers/resend/email_failed.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { ResendIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
buildEmailFailedOutputs,
|
||||
buildResendExtraFields,
|
||||
resendSetupInstructions,
|
||||
resendTriggerOptions,
|
||||
} from '@/triggers/resend/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
/**
|
||||
* Resend Email Failed Trigger
|
||||
* Triggers when an email fails to send.
|
||||
*/
|
||||
export const resendEmailFailedTrigger: TriggerConfig = {
|
||||
id: 'resend_email_failed',
|
||||
name: 'Resend Email Failed',
|
||||
provider: 'resend',
|
||||
description: 'Trigger workflow when an email fails to send',
|
||||
version: '1.0.0',
|
||||
icon: ResendIcon,
|
||||
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'resend_email_failed',
|
||||
triggerOptions: resendTriggerOptions,
|
||||
setupInstructions: resendSetupInstructions('email.failed'),
|
||||
extraFields: buildResendExtraFields('resend_email_failed'),
|
||||
}),
|
||||
|
||||
outputs: buildEmailFailedOutputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
}
|
||||
38
apps/sim/triggers/resend/email_opened.ts
Normal file
38
apps/sim/triggers/resend/email_opened.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { ResendIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
buildEmailOpenedOutputs,
|
||||
buildResendExtraFields,
|
||||
resendSetupInstructions,
|
||||
resendTriggerOptions,
|
||||
} from '@/triggers/resend/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
/**
|
||||
* Resend Email Opened Trigger
|
||||
* Triggers when a recipient opens an email.
|
||||
*/
|
||||
export const resendEmailOpenedTrigger: TriggerConfig = {
|
||||
id: 'resend_email_opened',
|
||||
name: 'Resend Email Opened',
|
||||
provider: 'resend',
|
||||
description: 'Trigger workflow when an email is opened',
|
||||
version: '1.0.0',
|
||||
icon: ResendIcon,
|
||||
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'resend_email_opened',
|
||||
triggerOptions: resendTriggerOptions,
|
||||
setupInstructions: resendSetupInstructions('email.opened'),
|
||||
extraFields: buildResendExtraFields('resend_email_opened'),
|
||||
}),
|
||||
|
||||
outputs: buildEmailOpenedOutputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
}
|
||||
41
apps/sim/triggers/resend/email_sent.ts
Normal file
41
apps/sim/triggers/resend/email_sent.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { ResendIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
buildEmailSentOutputs,
|
||||
buildResendExtraFields,
|
||||
resendSetupInstructions,
|
||||
resendTriggerOptions,
|
||||
} from '@/triggers/resend/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
/**
|
||||
* Resend Email Sent Trigger
|
||||
* Triggers when an email is sent by Resend.
|
||||
*
|
||||
* This is the PRIMARY trigger - it includes the dropdown for selecting trigger type.
|
||||
*/
|
||||
export const resendEmailSentTrigger: TriggerConfig = {
|
||||
id: 'resend_email_sent',
|
||||
name: 'Resend Email Sent',
|
||||
provider: 'resend',
|
||||
description: 'Trigger workflow when an email is sent',
|
||||
version: '1.0.0',
|
||||
icon: ResendIcon,
|
||||
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'resend_email_sent',
|
||||
triggerOptions: resendTriggerOptions,
|
||||
includeDropdown: true,
|
||||
setupInstructions: resendSetupInstructions('email.sent'),
|
||||
extraFields: buildResendExtraFields('resend_email_sent'),
|
||||
}),
|
||||
|
||||
outputs: buildEmailSentOutputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
}
|
||||
8
apps/sim/triggers/resend/index.ts
Normal file
8
apps/sim/triggers/resend/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export { resendEmailBouncedTrigger } from './email_bounced'
|
||||
export { resendEmailClickedTrigger } from './email_clicked'
|
||||
export { resendEmailComplainedTrigger } from './email_complained'
|
||||
export { resendEmailDeliveredTrigger } from './email_delivered'
|
||||
export { resendEmailFailedTrigger } from './email_failed'
|
||||
export { resendEmailOpenedTrigger } from './email_opened'
|
||||
export { resendEmailSentTrigger } from './email_sent'
|
||||
export { resendWebhookTrigger } from './webhook'
|
||||
188
apps/sim/triggers/resend/utils.ts
Normal file
188
apps/sim/triggers/resend/utils.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import type { TriggerOutput } from '@/triggers/types'
|
||||
|
||||
/**
|
||||
* Shared trigger dropdown options for all Resend triggers
|
||||
*/
|
||||
export const resendTriggerOptions = [
|
||||
{ label: 'Email Sent', id: 'resend_email_sent' },
|
||||
{ label: 'Email Delivered', id: 'resend_email_delivered' },
|
||||
{ label: 'Email Bounced', id: 'resend_email_bounced' },
|
||||
{ label: 'Email Complained', id: 'resend_email_complained' },
|
||||
{ label: 'Email Opened', id: 'resend_email_opened' },
|
||||
{ label: 'Email Clicked', id: 'resend_email_clicked' },
|
||||
{ label: 'Email Failed', id: 'resend_email_failed' },
|
||||
{ label: 'Generic Webhook (All Events)', id: 'resend_webhook' },
|
||||
]
|
||||
|
||||
/**
|
||||
* Generates setup instructions for Resend webhooks.
|
||||
* The webhook is automatically created in Resend when you save.
|
||||
*/
|
||||
export function resendSetupInstructions(eventType: string): string {
|
||||
const instructions = [
|
||||
'Enter your Resend API Key above.',
|
||||
'You can find your API key in Resend at <strong>Settings > API Keys</strong>. See the <a href="https://resend.com/docs/dashboard/api-keys/introduction" target="_blank" rel="noopener noreferrer">Resend API documentation</a> for details.',
|
||||
`Click <strong>"Save Configuration"</strong> to automatically create the webhook in Resend for <strong>${eventType}</strong> events.`,
|
||||
'The webhook will be automatically deleted when you remove this trigger.',
|
||||
]
|
||||
|
||||
return instructions
|
||||
.map(
|
||||
(instruction, index) =>
|
||||
`<div class="mb-3"><strong>${index + 1}.</strong> ${instruction}</div>`
|
||||
)
|
||||
.join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to build Resend-specific extra fields.
|
||||
* Includes API key (required).
|
||||
* Use with the generic buildTriggerSubBlocks from @/triggers.
|
||||
*/
|
||||
export function buildResendExtraFields(triggerId: string) {
|
||||
return [
|
||||
{
|
||||
id: 'apiKey',
|
||||
title: 'API Key',
|
||||
type: 'short-input' as const,
|
||||
placeholder: 'Enter your Resend API key (re_...)',
|
||||
description: 'Required to create the webhook in Resend.',
|
||||
password: true,
|
||||
paramVisibility: 'user-only' as const,
|
||||
required: true,
|
||||
mode: 'trigger' as const,
|
||||
condition: { field: 'selectedTriggerId', value: triggerId },
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Common fields present in all Resend email webhook payloads
|
||||
*/
|
||||
const commonEmailOutputs = {
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'Event type (e.g., email.sent, email.delivered)',
|
||||
},
|
||||
created_at: {
|
||||
type: 'string',
|
||||
description: 'Event creation timestamp (ISO 8601)',
|
||||
},
|
||||
email_id: {
|
||||
type: 'string',
|
||||
description: 'Unique email identifier',
|
||||
},
|
||||
from: {
|
||||
type: 'string',
|
||||
description: 'Sender email address',
|
||||
},
|
||||
subject: {
|
||||
type: 'string',
|
||||
description: 'Email subject line',
|
||||
},
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Recipient fields present in email webhook payloads
|
||||
*/
|
||||
const recipientOutputs = {
|
||||
to: {
|
||||
type: 'json',
|
||||
description: 'Array of recipient email addresses',
|
||||
},
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Build outputs for email sent events
|
||||
*/
|
||||
export function buildEmailSentOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
...commonEmailOutputs,
|
||||
...recipientOutputs,
|
||||
} as Record<string, TriggerOutput>
|
||||
}
|
||||
|
||||
/**
|
||||
* Build outputs for email delivered events
|
||||
*/
|
||||
export function buildEmailDeliveredOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
...commonEmailOutputs,
|
||||
...recipientOutputs,
|
||||
} as Record<string, TriggerOutput>
|
||||
}
|
||||
|
||||
/**
|
||||
* Build outputs for email bounced events
|
||||
*/
|
||||
export function buildEmailBouncedOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
...commonEmailOutputs,
|
||||
...recipientOutputs,
|
||||
bounceType: { type: 'string', description: 'Bounce type (e.g., Permanent)' },
|
||||
bounceSubType: { type: 'string', description: 'Bounce sub-type (e.g., Suppressed)' },
|
||||
bounceMessage: { type: 'string', description: 'Bounce error message' },
|
||||
} as Record<string, TriggerOutput>
|
||||
}
|
||||
|
||||
/**
|
||||
* Build outputs for email complained events
|
||||
*/
|
||||
export function buildEmailComplainedOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
...commonEmailOutputs,
|
||||
...recipientOutputs,
|
||||
} as Record<string, TriggerOutput>
|
||||
}
|
||||
|
||||
/**
|
||||
* Build outputs for email opened events
|
||||
*/
|
||||
export function buildEmailOpenedOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
...commonEmailOutputs,
|
||||
...recipientOutputs,
|
||||
} as Record<string, TriggerOutput>
|
||||
}
|
||||
|
||||
/**
|
||||
* Build outputs for email clicked events
|
||||
*/
|
||||
export function buildEmailClickedOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
...commonEmailOutputs,
|
||||
...recipientOutputs,
|
||||
clickIpAddress: { type: 'string', description: 'IP address of the click' },
|
||||
clickLink: { type: 'string', description: 'URL that was clicked' },
|
||||
clickTimestamp: { type: 'string', description: 'Click timestamp (ISO 8601)' },
|
||||
clickUserAgent: { type: 'string', description: 'Browser user agent string' },
|
||||
} as Record<string, TriggerOutput>
|
||||
}
|
||||
|
||||
/**
|
||||
* Build outputs for email failed events
|
||||
*/
|
||||
export function buildEmailFailedOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
...commonEmailOutputs,
|
||||
...recipientOutputs,
|
||||
} as Record<string, TriggerOutput>
|
||||
}
|
||||
|
||||
/**
|
||||
* Build outputs for generic webhook (all events).
|
||||
* Includes all possible fields across event types.
|
||||
*/
|
||||
export function buildResendOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
...commonEmailOutputs,
|
||||
...recipientOutputs,
|
||||
bounceType: { type: 'string', description: 'Bounce type (e.g., Permanent)' },
|
||||
bounceSubType: { type: 'string', description: 'Bounce sub-type (e.g., Suppressed)' },
|
||||
bounceMessage: { type: 'string', description: 'Bounce error message' },
|
||||
clickIpAddress: { type: 'string', description: 'IP address of the click' },
|
||||
clickLink: { type: 'string', description: 'URL that was clicked' },
|
||||
clickTimestamp: { type: 'string', description: 'Click timestamp (ISO 8601)' },
|
||||
clickUserAgent: { type: 'string', description: 'Browser user agent string' },
|
||||
} as Record<string, TriggerOutput>
|
||||
}
|
||||
38
apps/sim/triggers/resend/webhook.ts
Normal file
38
apps/sim/triggers/resend/webhook.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { ResendIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
buildResendExtraFields,
|
||||
buildResendOutputs,
|
||||
resendSetupInstructions,
|
||||
resendTriggerOptions,
|
||||
} from '@/triggers/resend/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
/**
|
||||
* Generic Resend Webhook Trigger
|
||||
* Captures all Resend webhook events
|
||||
*/
|
||||
export const resendWebhookTrigger: TriggerConfig = {
|
||||
id: 'resend_webhook',
|
||||
name: 'Resend Webhook (All Events)',
|
||||
provider: 'resend',
|
||||
description: 'Trigger workflow on any Resend webhook event',
|
||||
version: '1.0.0',
|
||||
icon: ResendIcon,
|
||||
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'resend_webhook',
|
||||
triggerOptions: resendTriggerOptions,
|
||||
setupInstructions: resendSetupInstructions('All Events'),
|
||||
extraFields: buildResendExtraFields('resend_webhook'),
|
||||
}),
|
||||
|
||||
outputs: buildResendOutputs(),
|
||||
|
||||
webhook: {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user