mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(attio): automatic webhook lifecycle management and tool fixes (#3327)
* fix(attio): use code subblock type for JSON input fields * fix(attio): correct people name attribute format in wand prompt example * fix(attio): improve wand prompt with correct attribute formats for all field types * fix(attio): use array format with full_name for personal-name attribute in wand prompt * fix(attio): use loose null checks to prevent sending null params to API * fix(attio): add offset param and make pagination fields advanced mode * fix(attio): remove redundant (optional) from placeholders * fix(attio): always send required workspace_access and workspace_member_access in create list * fix(attio): always send api_slug in create list, auto-generate from name if not provided * fix(attio): update api slug placeholder text * fix(tools): manage lifecycle for attio tools * updated docs * fix(attio): remove incorrect save button reference from setup instructions * fix(attio): log debug message when signature verification is skipped
This commit is contained in:
@@ -1012,8 +1012,8 @@ Update a webhook in Attio (target URL and/or subscriptions)
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `webhookId` | string | Yes | The webhook ID to update |
|
||||
| `targetUrl` | string | No | New HTTPS target URL |
|
||||
| `subscriptions` | string | No | New JSON array of subscriptions |
|
||||
| `targetUrl` | string | Yes | HTTPS target URL for webhook delivery |
|
||||
| `subscriptions` | string | Yes | JSON array of subscriptions, e.g. \[\{"event_type":"note.created"\}\] |
|
||||
|
||||
#### Output
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ export const AttioBlock: BlockConfig<AttioResponse> = {
|
||||
{
|
||||
id: 'values',
|
||||
title: 'Values',
|
||||
type: 'long-input',
|
||||
type: 'code',
|
||||
placeholder: '{"name": "Acme Corp", "domains": [{"domain": "acme.com"}]}',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
@@ -161,21 +161,25 @@ export const AttioBlock: BlockConfig<AttioResponse> = {
|
||||
Return ONLY the JSON object with Attio attribute values. No explanations, no markdown, no extra text.
|
||||
|
||||
### ATTIO VALUES STRUCTURE
|
||||
Keys are attribute slugs, values follow Attio's attribute format. Simple values can be strings; complex values are arrays of objects.
|
||||
Keys are attribute slugs. Most text attributes use array-of-objects format [{"value": "..."}]. Special attributes have their own format.
|
||||
|
||||
### COMMON PEOPLE ATTRIBUTES
|
||||
- name: [{"first_name": "...", "last_name": "..."}]
|
||||
- name: [{"first_name": "...", "last_name": "...", "full_name": "..."}] (personal-name type, full_name is required)
|
||||
- email_addresses: [{"email_address": "..."}]
|
||||
- phone_numbers: [{"original_phone_number": "...", "country_code": "US"}]
|
||||
- job_title, description, linkedin, twitter
|
||||
- job_title: [{"value": "..."}]
|
||||
- description: [{"value": "..."}]
|
||||
- linkedin: [{"value": "https://linkedin.com/in/..."}]
|
||||
- twitter: [{"value": "@handle"}]
|
||||
|
||||
### COMMON COMPANY ATTRIBUTES
|
||||
- name: [{"value": "..."}]
|
||||
- domains: [{"domain": "..."}]
|
||||
- description, primary_location, categories
|
||||
- description: [{"value": "..."}]
|
||||
- primary_location: [{"line_1": "...", "locality": "...", "region": "...", "postcode": "...", "country_code": "US"}]
|
||||
|
||||
### EXAMPLES
|
||||
Person: {"name": [{"first_name": "John", "last_name": "Doe"}], "email_addresses": [{"email_address": "john@example.com"}]}
|
||||
Person: {"name": [{"first_name": "John", "last_name": "Doe", "full_name": "John Doe"}], "email_addresses": [{"email_address": "john@example.com"}], "job_title": [{"value": "Engineer"}]}
|
||||
Company: {"name": [{"value": "Acme Corp"}], "domains": [{"domain": "acme.com"}]}`,
|
||||
placeholder: 'Describe the record values you want to set...',
|
||||
generationType: 'json-object',
|
||||
@@ -184,8 +188,8 @@ Company: {"name": [{"value": "Acme Corp"}], "domains": [{"domain": "acme.com"}]}
|
||||
{
|
||||
id: 'filter',
|
||||
title: 'Filter',
|
||||
type: 'long-input',
|
||||
placeholder: '{"name": "John Smith"} (optional)',
|
||||
type: 'code',
|
||||
placeholder: '{"name": "John Smith"}',
|
||||
condition: { field: 'operation', value: 'list_records' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
@@ -215,8 +219,8 @@ Empty (list all): {}`,
|
||||
{
|
||||
id: 'sorts',
|
||||
title: 'Sort',
|
||||
type: 'long-input',
|
||||
placeholder: '[{"direction":"asc","attribute":"name"}] (optional)',
|
||||
type: 'code',
|
||||
placeholder: '[{"direction":"asc","attribute":"name"}]',
|
||||
condition: { field: 'operation', value: 'list_records' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
@@ -332,7 +336,7 @@ YYYY-MM-DDTHH:mm:ss.SSSZ
|
||||
id: 'noteMeetingId',
|
||||
title: 'Meeting ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Link to a meeting (optional)',
|
||||
placeholder: 'Link to a meeting',
|
||||
condition: { field: 'operation', value: 'create_note' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
@@ -358,7 +362,7 @@ YYYY-MM-DDTHH:mm:ss.SSSZ
|
||||
id: 'taskDeadline',
|
||||
title: 'Deadline',
|
||||
type: 'short-input',
|
||||
placeholder: '2024-12-01T15:00:00.000Z (optional)',
|
||||
placeholder: '2024-12-01T15:00:00.000Z',
|
||||
condition: { field: 'operation', value: ['create_task', 'update_task'] },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
@@ -394,8 +398,8 @@ YYYY-MM-DDTHH:mm:ss.SSSZ
|
||||
{
|
||||
id: 'taskLinkedRecords',
|
||||
title: 'Linked Records',
|
||||
type: 'long-input',
|
||||
placeholder: '[{"target_object":"people","target_record_id":"..."}] (optional)',
|
||||
type: 'code',
|
||||
placeholder: '[{"target_object":"people","target_record_id":"..."}]',
|
||||
condition: { field: 'operation', value: ['create_task', 'update_task'] },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
@@ -420,9 +424,8 @@ Return ONLY the JSON array. No explanations, no markdown, no extra text.
|
||||
{
|
||||
id: 'taskAssignees',
|
||||
title: 'Assignees',
|
||||
type: 'long-input',
|
||||
placeholder:
|
||||
'[{"referenced_actor_type":"workspace-member","referenced_actor_id":"..."}] (optional)',
|
||||
type: 'code',
|
||||
placeholder: '[{"referenced_actor_type":"workspace-member","referenced_actor_id":"..."}]',
|
||||
condition: { field: 'operation', value: ['create_task', 'update_task'] },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
@@ -456,21 +459,21 @@ Return ONLY the JSON array. No explanations, no markdown, no extra text.
|
||||
id: 'taskFilterObject',
|
||||
title: 'Linked Object Type',
|
||||
type: 'short-input',
|
||||
placeholder: 'e.g. people, companies (optional)',
|
||||
placeholder: 'e.g. people, companies',
|
||||
condition: { field: 'operation', value: 'list_tasks' },
|
||||
},
|
||||
{
|
||||
id: 'taskFilterRecordId',
|
||||
title: 'Linked Record ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Filter by linked record ID (optional)',
|
||||
placeholder: 'Filter by linked record ID',
|
||||
condition: { field: 'operation', value: 'list_tasks' },
|
||||
},
|
||||
{
|
||||
id: 'taskFilterAssignee',
|
||||
title: 'Assignee',
|
||||
type: 'short-input',
|
||||
placeholder: 'Filter by assignee email or ID (optional)',
|
||||
placeholder: 'Filter by assignee email or ID',
|
||||
condition: { field: 'operation', value: 'list_tasks' },
|
||||
},
|
||||
{
|
||||
@@ -571,7 +574,7 @@ Return ONLY the JSON array. No explanations, no markdown, no extra text.
|
||||
id: 'listApiSlug',
|
||||
title: 'API Slug',
|
||||
type: 'short-input',
|
||||
placeholder: 'e.g. my_list (optional, auto-generated)',
|
||||
placeholder: 'e.g. my_list (auto-generated from name)',
|
||||
condition: { field: 'operation', value: ['create_list', 'update_list'] },
|
||||
mode: 'advanced',
|
||||
},
|
||||
@@ -622,8 +625,8 @@ Return ONLY the JSON array. No explanations, no markdown, no extra text.
|
||||
{
|
||||
id: 'entryValues',
|
||||
title: 'Entry Values',
|
||||
type: 'long-input',
|
||||
placeholder: '{"attribute_slug": "value"} (optional)',
|
||||
type: 'code',
|
||||
placeholder: '{"attribute_slug": "value"}',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['create_list_entry', 'update_list_entry'],
|
||||
@@ -652,8 +655,8 @@ Keys are list attribute slugs. Values follow Attio attribute format.
|
||||
{
|
||||
id: 'entryFilter',
|
||||
title: 'Filter',
|
||||
type: 'long-input',
|
||||
placeholder: '{"attribute": {"$operator": "value"}} (optional)',
|
||||
type: 'code',
|
||||
placeholder: '{"attribute": {"$operator": "value"}}',
|
||||
condition: { field: 'operation', value: 'query_list_entries' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
@@ -679,8 +682,8 @@ Logical: $and, $or, $not
|
||||
{
|
||||
id: 'entrySorts',
|
||||
title: 'Sort',
|
||||
type: 'long-input',
|
||||
placeholder: '[{"direction":"asc","attribute":"created_at"}] (optional)',
|
||||
type: 'code',
|
||||
placeholder: '[{"direction":"asc","attribute":"created_at"}]',
|
||||
condition: { field: 'operation', value: 'query_list_entries' },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
@@ -819,7 +822,7 @@ YYYY-MM-DDTHH:mm:ss.SSSZ
|
||||
id: 'threadFilterRecordId',
|
||||
title: 'Record ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Filter by record ID (optional)',
|
||||
placeholder: 'Filter by record ID',
|
||||
condition: { field: 'operation', value: 'list_threads' },
|
||||
},
|
||||
{
|
||||
@@ -833,7 +836,7 @@ YYYY-MM-DDTHH:mm:ss.SSSZ
|
||||
id: 'threadFilterEntryId',
|
||||
title: 'Entry ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Filter by entry ID (optional)',
|
||||
placeholder: 'Filter by entry ID',
|
||||
condition: { field: 'operation', value: 'list_threads' },
|
||||
},
|
||||
{
|
||||
@@ -865,15 +868,15 @@ YYYY-MM-DDTHH:mm:ss.SSSZ
|
||||
type: 'short-input',
|
||||
placeholder: 'https://example.com/webhook',
|
||||
condition: { field: 'operation', value: ['create_webhook', 'update_webhook'] },
|
||||
required: { field: 'operation', value: 'create_webhook' },
|
||||
required: { field: 'operation', value: ['create_webhook', 'update_webhook'] },
|
||||
},
|
||||
{
|
||||
id: 'webhookSubscriptions',
|
||||
title: 'Subscriptions',
|
||||
type: 'long-input',
|
||||
type: 'code',
|
||||
placeholder: '[{"event_type":"record.created","filter":{"object_id":"..."}}]',
|
||||
condition: { field: 'operation', value: ['create_webhook', 'update_webhook'] },
|
||||
required: { field: 'operation', value: 'create_webhook' },
|
||||
required: { field: 'operation', value: ['create_webhook', 'update_webhook'] },
|
||||
wandConfig: {
|
||||
enabled: true,
|
||||
maintainHistory: true,
|
||||
@@ -911,7 +914,8 @@ workspace-member.created
|
||||
id: 'limit',
|
||||
title: 'Limit',
|
||||
type: 'short-input',
|
||||
placeholder: 'Max results (optional)',
|
||||
placeholder: 'Max results',
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
@@ -925,6 +929,24 @@ workspace-member.created
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'offset',
|
||||
title: 'Offset',
|
||||
type: 'short-input',
|
||||
placeholder: 'Number of results to skip',
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'list_records',
|
||||
'list_notes',
|
||||
'list_tasks',
|
||||
'query_list_entries',
|
||||
'list_threads',
|
||||
'list_webhooks',
|
||||
],
|
||||
},
|
||||
},
|
||||
...getTrigger('attio_record_created').subBlocks,
|
||||
...getTrigger('attio_record_updated').subBlocks,
|
||||
...getTrigger('attio_record_deleted').subBlocks,
|
||||
@@ -1103,6 +1125,7 @@ workspace-member.created
|
||||
|
||||
// Shared params
|
||||
if (params.limit) cleanParams.limit = Number(params.limit)
|
||||
if (params.offset) cleanParams.offset = Number(params.offset)
|
||||
|
||||
return cleanParams
|
||||
},
|
||||
@@ -1164,6 +1187,7 @@ workspace-member.created
|
||||
webhookTargetUrl: { type: 'string', description: 'Webhook target URL' },
|
||||
webhookSubscriptions: { type: 'json', description: 'Webhook event subscriptions' },
|
||||
limit: { type: 'string', description: 'Maximum number of results' },
|
||||
offset: { type: 'string', description: 'Number of results to skip for pagination' },
|
||||
},
|
||||
|
||||
outputs: {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { convertSquareBracketsToTwiML } from '@/lib/webhooks/utils'
|
||||
import {
|
||||
handleSlackChallenge,
|
||||
handleWhatsAppVerification,
|
||||
validateAttioSignature,
|
||||
validateCalcomSignature,
|
||||
validateCirclebackSignature,
|
||||
validateFirefliesSignature,
|
||||
@@ -597,6 +598,33 @@ export async function verifyProviderAuth(
|
||||
}
|
||||
}
|
||||
|
||||
if (foundWebhook.provider === 'attio') {
|
||||
const secret = providerConfig.webhookSecret as string | undefined
|
||||
|
||||
if (!secret) {
|
||||
logger.debug(
|
||||
`[${requestId}] Attio webhook ${foundWebhook.id} has no signing secret, skipping signature verification`
|
||||
)
|
||||
} else {
|
||||
const signature = request.headers.get('Attio-Signature')
|
||||
|
||||
if (!signature) {
|
||||
logger.warn(`[${requestId}] Attio webhook missing signature header`)
|
||||
return new NextResponse('Unauthorized - Missing Attio signature', { status: 401 })
|
||||
}
|
||||
|
||||
const isValidSignature = validateAttioSignature(secret, signature, rawBody)
|
||||
|
||||
if (!isValidSignature) {
|
||||
logger.warn(`[${requestId}] Attio signature verification failed`, {
|
||||
signatureLength: signature.length,
|
||||
secretLength: secret.length,
|
||||
})
|
||||
return new NextResponse('Unauthorized - Invalid Attio signature', { status: 401 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (foundWebhook.provider === 'linear') {
|
||||
const secret = providerConfig.webhookSecret as string | undefined
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ const calendlyLogger = createLogger('CalendlyWebhook')
|
||||
const grainLogger = createLogger('GrainWebhook')
|
||||
const lemlistLogger = createLogger('LemlistWebhook')
|
||||
const webflowLogger = createLogger('WebflowWebhook')
|
||||
const attioLogger = createLogger('AttioWebhook')
|
||||
const providerSubscriptionsLogger = createLogger('WebhookProviderSubscriptions')
|
||||
|
||||
function getProviderConfig(webhook: any): Record<string, any> {
|
||||
@@ -976,6 +977,196 @@ export async function deleteWebflowWebhook(
|
||||
}
|
||||
}
|
||||
|
||||
export async function createAttioWebhookSubscription(
|
||||
userId: string,
|
||||
webhookData: any,
|
||||
requestId: string
|
||||
): Promise<{ externalId: string; webhookSecret: string } | undefined> {
|
||||
try {
|
||||
const { path, providerConfig } = webhookData
|
||||
const { triggerId, credentialId } = providerConfig || {}
|
||||
|
||||
if (!credentialId) {
|
||||
attioLogger.warn(`[${requestId}] Missing credentialId for Attio webhook creation.`, {
|
||||
webhookId: webhookData.id,
|
||||
})
|
||||
throw new Error(
|
||||
'Attio account connection required. Please connect your Attio account in the trigger configuration and try again.'
|
||||
)
|
||||
}
|
||||
|
||||
const credentialOwner = await getCredentialOwner(credentialId, requestId)
|
||||
const accessToken = credentialOwner
|
||||
? await refreshAccessTokenIfNeeded(
|
||||
credentialOwner.accountId,
|
||||
credentialOwner.userId,
|
||||
requestId
|
||||
)
|
||||
: null
|
||||
|
||||
if (!accessToken) {
|
||||
attioLogger.warn(
|
||||
`[${requestId}] Could not retrieve Attio access token for user ${userId}. Cannot create webhook.`
|
||||
)
|
||||
throw new Error(
|
||||
'Attio account connection required. Please connect your Attio account in the trigger configuration and try again.'
|
||||
)
|
||||
}
|
||||
|
||||
const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}`
|
||||
|
||||
const { TRIGGER_EVENT_MAP } = await import('@/triggers/attio/utils')
|
||||
|
||||
let subscriptions: Array<{ event_type: string }> = []
|
||||
if (triggerId === 'attio_webhook') {
|
||||
const allEvents = new Set<string>()
|
||||
for (const events of Object.values(TRIGGER_EVENT_MAP)) {
|
||||
for (const event of events) {
|
||||
allEvents.add(event)
|
||||
}
|
||||
}
|
||||
subscriptions = Array.from(allEvents).map((event_type) => ({ event_type }))
|
||||
} else {
|
||||
const events = TRIGGER_EVENT_MAP[triggerId]
|
||||
if (!events || events.length === 0) {
|
||||
attioLogger.warn(`[${requestId}] No event types mapped for trigger ${triggerId}`, {
|
||||
webhookId: webhookData.id,
|
||||
})
|
||||
throw new Error(`Unknown Attio trigger type: ${triggerId}`)
|
||||
}
|
||||
subscriptions = events.map((event_type) => ({ event_type }))
|
||||
}
|
||||
|
||||
const requestBody = {
|
||||
target_url: notificationUrl,
|
||||
subscriptions,
|
||||
}
|
||||
|
||||
const attioResponse = await fetch('https://api.attio.com/v2/webhooks', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
|
||||
if (!attioResponse.ok) {
|
||||
const errorBody = await attioResponse.json().catch(() => ({}))
|
||||
attioLogger.error(
|
||||
`[${requestId}] Failed to create webhook in Attio for webhook ${webhookData.id}. Status: ${attioResponse.status}`,
|
||||
{ response: errorBody }
|
||||
)
|
||||
|
||||
let userFriendlyMessage = 'Failed to create webhook subscription in Attio'
|
||||
if (attioResponse.status === 401) {
|
||||
userFriendlyMessage = 'Attio authentication failed. Please reconnect your Attio account.'
|
||||
} else if (attioResponse.status === 403) {
|
||||
userFriendlyMessage =
|
||||
'Attio access denied. Please ensure your integration has webhook permissions.'
|
||||
}
|
||||
|
||||
throw new Error(userFriendlyMessage)
|
||||
}
|
||||
|
||||
const responseBody = await attioResponse.json()
|
||||
const data = responseBody.data || responseBody
|
||||
const webhookId = data.id?.webhook_id || data.webhook_id || data.id
|
||||
const secret = data.secret
|
||||
|
||||
if (!webhookId) {
|
||||
attioLogger.error(
|
||||
`[${requestId}] Attio webhook created but no webhook_id returned for webhook ${webhookData.id}`,
|
||||
{ response: responseBody }
|
||||
)
|
||||
throw new Error('Attio webhook creation succeeded but no webhook ID was returned')
|
||||
}
|
||||
|
||||
if (!secret) {
|
||||
attioLogger.warn(
|
||||
`[${requestId}] Attio webhook created but no secret returned for webhook ${webhookData.id}. Signature verification will be skipped.`,
|
||||
{ response: responseBody }
|
||||
)
|
||||
}
|
||||
|
||||
attioLogger.info(
|
||||
`[${requestId}] Successfully created webhook in Attio for webhook ${webhookData.id}.`,
|
||||
{ attioWebhookId: webhookId }
|
||||
)
|
||||
|
||||
return { externalId: webhookId, webhookSecret: secret || '' }
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
attioLogger.error(
|
||||
`[${requestId}] Exception during Attio webhook creation for webhook ${webhookData.id}.`,
|
||||
{ message }
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteAttioWebhook(
|
||||
webhook: any,
|
||||
_workflow: any,
|
||||
requestId: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
const config = getProviderConfig(webhook)
|
||||
const externalId = config.externalId as string | undefined
|
||||
const credentialId = config.credentialId as string | undefined
|
||||
|
||||
if (!externalId) {
|
||||
attioLogger.warn(
|
||||
`[${requestId}] Missing externalId for Attio webhook deletion ${webhook.id}, skipping cleanup`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (!credentialId) {
|
||||
attioLogger.warn(
|
||||
`[${requestId}] Missing credentialId for Attio webhook deletion ${webhook.id}, skipping cleanup`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const credentialOwner = await getCredentialOwner(credentialId, requestId)
|
||||
const accessToken = credentialOwner
|
||||
? await refreshAccessTokenIfNeeded(
|
||||
credentialOwner.accountId,
|
||||
credentialOwner.userId,
|
||||
requestId
|
||||
)
|
||||
: null
|
||||
|
||||
if (!accessToken) {
|
||||
attioLogger.warn(
|
||||
`[${requestId}] Could not retrieve Attio access token. Cannot delete webhook.`,
|
||||
{ webhookId: webhook.id }
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const attioResponse = await fetch(`https://api.attio.com/v2/webhooks/${externalId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!attioResponse.ok && attioResponse.status !== 404) {
|
||||
const responseBody = await attioResponse.json().catch(() => ({}))
|
||||
attioLogger.warn(
|
||||
`[${requestId}] Failed to delete Attio webhook (non-fatal): ${attioResponse.status}`,
|
||||
{ response: responseBody }
|
||||
)
|
||||
} else {
|
||||
attioLogger.info(`[${requestId}] Successfully deleted Attio webhook ${externalId}`)
|
||||
}
|
||||
} catch (error) {
|
||||
attioLogger.warn(`[${requestId}] Error deleting Attio webhook (non-fatal)`, error)
|
||||
}
|
||||
}
|
||||
|
||||
export async function createGrainWebhookSubscription(
|
||||
_request: NextRequest,
|
||||
webhookData: any,
|
||||
@@ -1611,6 +1802,7 @@ type RecreateCheckInput = {
|
||||
/** Providers that create external webhook subscriptions */
|
||||
const PROVIDERS_WITH_EXTERNAL_SUBSCRIPTIONS = new Set([
|
||||
'airtable',
|
||||
'attio',
|
||||
'calendly',
|
||||
'webflow',
|
||||
'typeform',
|
||||
@@ -1626,6 +1818,7 @@ const SYSTEM_MANAGED_FIELDS = new Set([
|
||||
'externalSubscriptionId',
|
||||
'eventTypes',
|
||||
'webhookTag',
|
||||
'webhookSecret',
|
||||
'historyId',
|
||||
'lastCheckedTimestamp',
|
||||
'setupCompleted',
|
||||
@@ -1686,6 +1879,16 @@ export async function createExternalWebhookSubscription(
|
||||
updatedProviderConfig = { ...updatedProviderConfig, externalId }
|
||||
externalSubscriptionCreated = true
|
||||
}
|
||||
} else if (provider === 'attio') {
|
||||
const result = await createAttioWebhookSubscription(userId, webhookData, requestId)
|
||||
if (result) {
|
||||
updatedProviderConfig = {
|
||||
...updatedProviderConfig,
|
||||
externalId: result.externalId,
|
||||
webhookSecret: result.webhookSecret,
|
||||
}
|
||||
externalSubscriptionCreated = true
|
||||
}
|
||||
} else if (provider === 'calendly') {
|
||||
const externalId = await createCalendlyWebhookSubscription(webhookData, requestId)
|
||||
if (externalId) {
|
||||
@@ -1736,7 +1939,7 @@ export async function createExternalWebhookSubscription(
|
||||
|
||||
/**
|
||||
* Clean up external webhook subscriptions for a webhook
|
||||
* Handles Airtable, Teams, Telegram, Typeform, Calendly, Grain, and Lemlist cleanup
|
||||
* Handles Airtable, Attio, Teams, Telegram, Typeform, Calendly, Grain, and Lemlist cleanup
|
||||
* Don't fail deletion if cleanup fails
|
||||
*/
|
||||
export async function cleanupExternalWebhook(
|
||||
@@ -1746,6 +1949,8 @@ export async function cleanupExternalWebhook(
|
||||
): Promise<void> {
|
||||
if (webhook.provider === 'airtable') {
|
||||
await deleteAirtableWebhook(webhook, workflow, requestId)
|
||||
} else if (webhook.provider === 'attio') {
|
||||
await deleteAttioWebhook(webhook, workflow, requestId)
|
||||
} else if (webhook.provider === 'microsoft-teams') {
|
||||
await deleteTeamsSubscription(webhook, workflow, requestId)
|
||||
} else if (webhook.provider === 'telegram') {
|
||||
|
||||
@@ -1395,6 +1395,41 @@ export function validateLinearSignature(secret: string, signature: string, body:
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an Attio webhook request signature using HMAC SHA-256
|
||||
* @param secret - Attio webhook signing secret (plain text)
|
||||
* @param signature - Attio-Signature header value (hex-encoded HMAC SHA-256 signature)
|
||||
* @param body - Raw request body string
|
||||
* @returns Whether the signature is valid
|
||||
*/
|
||||
export function validateAttioSignature(secret: string, signature: string, body: string): boolean {
|
||||
try {
|
||||
if (!secret || !signature || !body) {
|
||||
logger.warn('Attio signature validation missing required fields', {
|
||||
hasSecret: !!secret,
|
||||
hasSignature: !!signature,
|
||||
hasBody: !!body,
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex')
|
||||
|
||||
logger.debug('Attio signature comparison', {
|
||||
computedSignature: `${computedHash.substring(0, 10)}...`,
|
||||
providedSignature: `${signature.substring(0, 10)}...`,
|
||||
computedLength: computedHash.length,
|
||||
providedLength: signature.length,
|
||||
match: computedHash === signature,
|
||||
})
|
||||
|
||||
return safeCompare(computedHash, signature)
|
||||
} catch (error) {
|
||||
logger.error('Error validating Attio signature:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a Circleback webhook request signature using HMAC SHA-256
|
||||
* @param secret - Circleback signing secret (plain text)
|
||||
|
||||
@@ -65,12 +65,19 @@ export const attioCreateListTool: ToolConfig<AttioCreateListParams, AttioCreateL
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const apiSlug =
|
||||
params.apiSlug ||
|
||||
params.name
|
||||
?.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '_')
|
||||
.replace(/^_|_$/g, '')
|
||||
const data: Record<string, unknown> = {
|
||||
name: params.name,
|
||||
api_slug: apiSlug,
|
||||
parent_object: params.parentObject,
|
||||
workspace_access: params.workspaceAccess ?? null,
|
||||
workspace_member_access: [] as unknown[],
|
||||
}
|
||||
if (params.apiSlug) data.api_slug = params.apiSlug
|
||||
if (params.workspaceAccess) data.workspace_access = params.workspaceAccess
|
||||
if (params.workspaceMemberAccess) {
|
||||
try {
|
||||
data.workspace_member_access =
|
||||
|
||||
@@ -60,20 +60,22 @@ export const attioCreateListEntryTool: ToolConfig<
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const data: Record<string, unknown> = {
|
||||
parent_record_id: params.parentRecordId,
|
||||
parent_object: params.parentObject,
|
||||
}
|
||||
let entryValues: unknown = {}
|
||||
if (params.entryValues) {
|
||||
try {
|
||||
data.entry_values =
|
||||
entryValues =
|
||||
typeof params.entryValues === 'string'
|
||||
? JSON.parse(params.entryValues)
|
||||
: params.entryValues
|
||||
} catch {
|
||||
data.entry_values = {}
|
||||
entryValues = {}
|
||||
}
|
||||
}
|
||||
const data: Record<string, unknown> = {
|
||||
parent_record_id: params.parentRecordId,
|
||||
parent_object: params.parentObject,
|
||||
entry_values: entryValues,
|
||||
}
|
||||
return { data }
|
||||
},
|
||||
},
|
||||
|
||||
@@ -83,7 +83,7 @@ export const attioCreateNoteTool: ToolConfig<AttioCreateNoteParams, AttioCreateN
|
||||
content: params.content,
|
||||
}
|
||||
if (params.createdAt) body.created_at = params.createdAt
|
||||
if (params.meetingId !== undefined) body.meeting_id = params.meetingId || null
|
||||
if (params.meetingId != null) body.meeting_id = params.meetingId || null
|
||||
return { data: body }
|
||||
},
|
||||
},
|
||||
|
||||
@@ -54,8 +54,8 @@ export const attioListNotesTool: ToolConfig<AttioListNotesParams, AttioListNotes
|
||||
const searchParams = new URLSearchParams()
|
||||
if (params.parentObject) searchParams.set('parent_object', params.parentObject)
|
||||
if (params.parentRecordId) searchParams.set('parent_record_id', params.parentRecordId)
|
||||
if (params.limit !== undefined) searchParams.set('limit', String(params.limit))
|
||||
if (params.offset !== undefined) searchParams.set('offset', String(params.offset))
|
||||
if (params.limit != null) searchParams.set('limit', String(params.limit))
|
||||
if (params.offset != null) searchParams.set('offset', String(params.offset))
|
||||
const qs = searchParams.toString()
|
||||
return `https://api.attio.com/v2/notes${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
|
||||
@@ -78,8 +78,8 @@ export const attioListRecordsTool: ToolConfig<AttioListRecordsParams, AttioListR
|
||||
body.sorts = params.sorts
|
||||
}
|
||||
}
|
||||
if (params.limit !== undefined) body.limit = params.limit
|
||||
if (params.offset !== undefined) body.offset = params.offset
|
||||
if (params.limit != null) body.limit = params.limit
|
||||
if (params.offset != null) body.offset = params.offset
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
@@ -73,12 +73,12 @@ export const attioListTasksTool: ToolConfig<AttioListTasksParams, AttioListTasks
|
||||
if (params.linkedObject) searchParams.set('linked_object', params.linkedObject)
|
||||
if (params.linkedRecordId) searchParams.set('linked_record_id', params.linkedRecordId)
|
||||
if (params.assignee) searchParams.set('assignee', params.assignee)
|
||||
if (params.isCompleted !== undefined) {
|
||||
if (params.isCompleted != null) {
|
||||
searchParams.set('is_completed', String(params.isCompleted))
|
||||
}
|
||||
if (params.sort) searchParams.set('sort', params.sort)
|
||||
if (params.limit !== undefined) searchParams.set('limit', String(params.limit))
|
||||
if (params.offset !== undefined) searchParams.set('offset', String(params.offset))
|
||||
if (params.limit != null) searchParams.set('limit', String(params.limit))
|
||||
if (params.offset != null) searchParams.set('offset', String(params.offset))
|
||||
const qs = searchParams.toString()
|
||||
return `https://api.attio.com/v2/tasks${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
|
||||
@@ -67,8 +67,8 @@ export const attioListThreadsTool: ToolConfig<AttioListThreadsParams, AttioListT
|
||||
if (params.object) searchParams.set('object', params.object)
|
||||
if (params.entryId) searchParams.set('entry_id', params.entryId)
|
||||
if (params.list) searchParams.set('list', params.list)
|
||||
if (params.limit !== undefined) searchParams.set('limit', String(params.limit))
|
||||
if (params.offset !== undefined) searchParams.set('offset', String(params.offset))
|
||||
if (params.limit != null) searchParams.set('limit', String(params.limit))
|
||||
if (params.offset != null) searchParams.set('offset', String(params.offset))
|
||||
const qs = searchParams.toString()
|
||||
return `https://api.attio.com/v2/threads${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
|
||||
@@ -41,8 +41,8 @@ export const attioListWebhooksTool: ToolConfig<AttioListWebhooksParams, AttioLis
|
||||
request: {
|
||||
url: (params) => {
|
||||
const searchParams = new URLSearchParams()
|
||||
if (params.limit !== undefined) searchParams.set('limit', String(params.limit))
|
||||
if (params.offset !== undefined) searchParams.set('offset', String(params.offset))
|
||||
if (params.limit != null) searchParams.set('limit', String(params.limit))
|
||||
if (params.offset != null) searchParams.set('offset', String(params.offset))
|
||||
const qs = searchParams.toString()
|
||||
return `https://api.attio.com/v2/webhooks${qs ? `?${qs}` : ''}`
|
||||
},
|
||||
|
||||
@@ -83,8 +83,8 @@ export const attioQueryListEntriesTool: ToolConfig<
|
||||
body.sorts = []
|
||||
}
|
||||
}
|
||||
if (params.limit !== undefined) body.limit = params.limit
|
||||
if (params.offset !== undefined) body.offset = params.offset
|
||||
if (params.limit != null) body.limit = params.limit
|
||||
if (params.offset != null) body.offset = params.offset
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
@@ -66,10 +66,10 @@ export const attioUpdateListTool: ToolConfig<AttioUpdateListParams, AttioUpdateL
|
||||
}),
|
||||
body: (params) => {
|
||||
const data: Record<string, unknown> = {}
|
||||
if (params.name !== undefined) data.name = params.name
|
||||
if (params.apiSlug !== undefined) data.api_slug = params.apiSlug
|
||||
if (params.workspaceAccess !== undefined) data.workspace_access = params.workspaceAccess
|
||||
if (params.workspaceMemberAccess !== undefined) {
|
||||
if (params.name != null) data.name = params.name
|
||||
if (params.apiSlug != null) data.api_slug = params.apiSlug
|
||||
if (params.workspaceAccess != null) data.workspace_access = params.workspaceAccess
|
||||
if (params.workspaceMemberAccess != null) {
|
||||
try {
|
||||
data.workspace_member_access =
|
||||
typeof params.workspaceMemberAccess === 'string'
|
||||
|
||||
@@ -59,9 +59,9 @@ export const attioUpdateObjectTool: ToolConfig<AttioUpdateObjectParams, AttioUpd
|
||||
}),
|
||||
body: (params) => {
|
||||
const data: Record<string, unknown> = {}
|
||||
if (params.apiSlug !== undefined) data.api_slug = params.apiSlug
|
||||
if (params.singularNoun !== undefined) data.singular_noun = params.singularNoun
|
||||
if (params.pluralNoun !== undefined) data.plural_noun = params.pluralNoun
|
||||
if (params.apiSlug != null) data.api_slug = params.apiSlug
|
||||
if (params.singularNoun != null) data.singular_noun = params.singularNoun
|
||||
if (params.pluralNoun != null) data.plural_noun = params.pluralNoun
|
||||
return { data }
|
||||
},
|
||||
},
|
||||
|
||||
@@ -64,8 +64,8 @@ export const attioUpdateTaskTool: ToolConfig<AttioUpdateTaskParams, AttioUpdateT
|
||||
}),
|
||||
body: (params) => {
|
||||
const data: Record<string, unknown> = {}
|
||||
if (params.deadlineAt !== undefined) data.deadline_at = params.deadlineAt || null
|
||||
if (params.isCompleted !== undefined) data.is_completed = params.isCompleted
|
||||
if (params.deadlineAt != null) data.deadline_at = params.deadlineAt || null
|
||||
if (params.isCompleted != null) data.is_completed = params.isCompleted
|
||||
if (params.linkedRecords) {
|
||||
try {
|
||||
data.linked_records =
|
||||
|
||||
@@ -34,15 +34,15 @@ export const attioUpdateWebhookTool: ToolConfig<
|
||||
},
|
||||
targetUrl: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New HTTPS target URL',
|
||||
description: 'HTTPS target URL for webhook delivery',
|
||||
},
|
||||
subscriptions: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New JSON array of subscriptions',
|
||||
description: 'JSON array of subscriptions, e.g. [{"event_type":"note.created"}]',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -54,18 +54,21 @@ export const attioUpdateWebhookTool: ToolConfig<
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const data: Record<string, unknown> = {}
|
||||
if (params.targetUrl !== undefined) data.target_url = params.targetUrl
|
||||
let subscriptions: unknown = []
|
||||
if (params.subscriptions) {
|
||||
try {
|
||||
data.subscriptions =
|
||||
subscriptions =
|
||||
typeof params.subscriptions === 'string'
|
||||
? JSON.parse(params.subscriptions)
|
||||
: params.subscriptions
|
||||
} catch {
|
||||
data.subscriptions = []
|
||||
subscriptions = []
|
||||
}
|
||||
}
|
||||
const data: Record<string, unknown> = {
|
||||
target_url: params.targetUrl,
|
||||
subscriptions,
|
||||
}
|
||||
return { data }
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { AttioIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
attioSetupInstructions,
|
||||
attioTriggerOptions,
|
||||
buildAttioExtraFields,
|
||||
buildCommentOutputs,
|
||||
} from '@/triggers/attio/utils'
|
||||
import { buildAttioTriggerSubBlocks, buildCommentOutputs } from '@/triggers/attio/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
/**
|
||||
@@ -21,12 +15,7 @@ export const attioCommentCreatedTrigger: TriggerConfig = {
|
||||
version: '1.0.0',
|
||||
icon: AttioIcon,
|
||||
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'attio_comment_created',
|
||||
triggerOptions: attioTriggerOptions,
|
||||
setupInstructions: attioSetupInstructions('comment.created'),
|
||||
extraFields: buildAttioExtraFields('attio_comment_created'),
|
||||
}),
|
||||
subBlocks: buildAttioTriggerSubBlocks('attio_comment_created'),
|
||||
|
||||
outputs: buildCommentOutputs(),
|
||||
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { AttioIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
attioSetupInstructions,
|
||||
attioTriggerOptions,
|
||||
buildAttioExtraFields,
|
||||
buildCommentOutputs,
|
||||
} from '@/triggers/attio/utils'
|
||||
import { buildAttioTriggerSubBlocks, buildCommentOutputs } from '@/triggers/attio/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
/**
|
||||
@@ -21,12 +15,7 @@ export const attioCommentDeletedTrigger: TriggerConfig = {
|
||||
version: '1.0.0',
|
||||
icon: AttioIcon,
|
||||
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'attio_comment_deleted',
|
||||
triggerOptions: attioTriggerOptions,
|
||||
setupInstructions: attioSetupInstructions('comment.deleted'),
|
||||
extraFields: buildAttioExtraFields('attio_comment_deleted'),
|
||||
}),
|
||||
subBlocks: buildAttioTriggerSubBlocks('attio_comment_deleted'),
|
||||
|
||||
outputs: buildCommentOutputs(),
|
||||
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { AttioIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
attioSetupInstructions,
|
||||
attioTriggerOptions,
|
||||
buildAttioExtraFields,
|
||||
buildCommentOutputs,
|
||||
} from '@/triggers/attio/utils'
|
||||
import { buildAttioTriggerSubBlocks, buildCommentOutputs } from '@/triggers/attio/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
/**
|
||||
@@ -21,12 +15,7 @@ export const attioCommentResolvedTrigger: TriggerConfig = {
|
||||
version: '1.0.0',
|
||||
icon: AttioIcon,
|
||||
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'attio_comment_resolved',
|
||||
triggerOptions: attioTriggerOptions,
|
||||
setupInstructions: attioSetupInstructions('comment.resolved'),
|
||||
extraFields: buildAttioExtraFields('attio_comment_resolved'),
|
||||
}),
|
||||
subBlocks: buildAttioTriggerSubBlocks('attio_comment_resolved'),
|
||||
|
||||
outputs: buildCommentOutputs(),
|
||||
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { AttioIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
attioSetupInstructions,
|
||||
attioTriggerOptions,
|
||||
buildAttioExtraFields,
|
||||
buildCommentOutputs,
|
||||
} from '@/triggers/attio/utils'
|
||||
import { buildAttioTriggerSubBlocks, buildCommentOutputs } from '@/triggers/attio/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
/**
|
||||
@@ -21,12 +15,7 @@ export const attioCommentUnresolvedTrigger: TriggerConfig = {
|
||||
version: '1.0.0',
|
||||
icon: AttioIcon,
|
||||
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'attio_comment_unresolved',
|
||||
triggerOptions: attioTriggerOptions,
|
||||
setupInstructions: attioSetupInstructions('comment.unresolved'),
|
||||
extraFields: buildAttioExtraFields('attio_comment_unresolved'),
|
||||
}),
|
||||
subBlocks: buildAttioTriggerSubBlocks('attio_comment_unresolved'),
|
||||
|
||||
outputs: buildCommentOutputs(),
|
||||
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { AttioIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
attioSetupInstructions,
|
||||
attioTriggerOptions,
|
||||
buildAttioExtraFields,
|
||||
buildListEntryOutputs,
|
||||
} from '@/triggers/attio/utils'
|
||||
import { buildAttioTriggerSubBlocks, buildListEntryOutputs } from '@/triggers/attio/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
/**
|
||||
@@ -21,12 +15,7 @@ export const attioListEntryCreatedTrigger: TriggerConfig = {
|
||||
version: '1.0.0',
|
||||
icon: AttioIcon,
|
||||
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'attio_list_entry_created',
|
||||
triggerOptions: attioTriggerOptions,
|
||||
setupInstructions: attioSetupInstructions('list-entry.created'),
|
||||
extraFields: buildAttioExtraFields('attio_list_entry_created'),
|
||||
}),
|
||||
subBlocks: buildAttioTriggerSubBlocks('attio_list_entry_created'),
|
||||
|
||||
outputs: buildListEntryOutputs(),
|
||||
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { AttioIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
attioSetupInstructions,
|
||||
attioTriggerOptions,
|
||||
buildAttioExtraFields,
|
||||
buildListEntryOutputs,
|
||||
} from '@/triggers/attio/utils'
|
||||
import { buildAttioTriggerSubBlocks, buildListEntryOutputs } from '@/triggers/attio/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
/**
|
||||
@@ -21,12 +15,7 @@ export const attioListEntryDeletedTrigger: TriggerConfig = {
|
||||
version: '1.0.0',
|
||||
icon: AttioIcon,
|
||||
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'attio_list_entry_deleted',
|
||||
triggerOptions: attioTriggerOptions,
|
||||
setupInstructions: attioSetupInstructions('list-entry.deleted'),
|
||||
extraFields: buildAttioExtraFields('attio_list_entry_deleted'),
|
||||
}),
|
||||
subBlocks: buildAttioTriggerSubBlocks('attio_list_entry_deleted'),
|
||||
|
||||
outputs: buildListEntryOutputs(),
|
||||
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { AttioIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
attioSetupInstructions,
|
||||
attioTriggerOptions,
|
||||
buildAttioExtraFields,
|
||||
buildListEntryUpdatedOutputs,
|
||||
} from '@/triggers/attio/utils'
|
||||
import { buildAttioTriggerSubBlocks, buildListEntryUpdatedOutputs } from '@/triggers/attio/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
/**
|
||||
@@ -21,12 +15,7 @@ export const attioListEntryUpdatedTrigger: TriggerConfig = {
|
||||
version: '1.0.0',
|
||||
icon: AttioIcon,
|
||||
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'attio_list_entry_updated',
|
||||
triggerOptions: attioTriggerOptions,
|
||||
setupInstructions: attioSetupInstructions('list-entry.updated'),
|
||||
extraFields: buildAttioExtraFields('attio_list_entry_updated'),
|
||||
}),
|
||||
subBlocks: buildAttioTriggerSubBlocks('attio_list_entry_updated'),
|
||||
|
||||
outputs: buildListEntryUpdatedOutputs(),
|
||||
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { AttioIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
attioSetupInstructions,
|
||||
attioTriggerOptions,
|
||||
buildAttioExtraFields,
|
||||
buildNoteOutputs,
|
||||
} from '@/triggers/attio/utils'
|
||||
import { buildAttioTriggerSubBlocks, buildNoteOutputs } from '@/triggers/attio/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
/**
|
||||
@@ -21,12 +15,7 @@ export const attioNoteCreatedTrigger: TriggerConfig = {
|
||||
version: '1.0.0',
|
||||
icon: AttioIcon,
|
||||
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'attio_note_created',
|
||||
triggerOptions: attioTriggerOptions,
|
||||
setupInstructions: attioSetupInstructions('note.created'),
|
||||
extraFields: buildAttioExtraFields('attio_note_created'),
|
||||
}),
|
||||
subBlocks: buildAttioTriggerSubBlocks('attio_note_created'),
|
||||
|
||||
outputs: buildNoteOutputs(),
|
||||
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { AttioIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
attioSetupInstructions,
|
||||
attioTriggerOptions,
|
||||
buildAttioExtraFields,
|
||||
buildNoteOutputs,
|
||||
} from '@/triggers/attio/utils'
|
||||
import { buildAttioTriggerSubBlocks, buildNoteOutputs } from '@/triggers/attio/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
/**
|
||||
@@ -21,12 +15,7 @@ export const attioNoteDeletedTrigger: TriggerConfig = {
|
||||
version: '1.0.0',
|
||||
icon: AttioIcon,
|
||||
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'attio_note_deleted',
|
||||
triggerOptions: attioTriggerOptions,
|
||||
setupInstructions: attioSetupInstructions('note.deleted'),
|
||||
extraFields: buildAttioExtraFields('attio_note_deleted'),
|
||||
}),
|
||||
subBlocks: buildAttioTriggerSubBlocks('attio_note_deleted'),
|
||||
|
||||
outputs: buildNoteOutputs(),
|
||||
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { AttioIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
attioSetupInstructions,
|
||||
attioTriggerOptions,
|
||||
buildAttioExtraFields,
|
||||
buildNoteOutputs,
|
||||
} from '@/triggers/attio/utils'
|
||||
import { buildAttioTriggerSubBlocks, buildNoteOutputs } from '@/triggers/attio/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
/**
|
||||
@@ -21,12 +15,7 @@ export const attioNoteUpdatedTrigger: TriggerConfig = {
|
||||
version: '1.0.0',
|
||||
icon: AttioIcon,
|
||||
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'attio_note_updated',
|
||||
triggerOptions: attioTriggerOptions,
|
||||
setupInstructions: attioSetupInstructions('note.updated'),
|
||||
extraFields: buildAttioExtraFields('attio_note_updated'),
|
||||
}),
|
||||
subBlocks: buildAttioTriggerSubBlocks('attio_note_updated'),
|
||||
|
||||
outputs: buildNoteOutputs(),
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { AttioIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
attioSetupInstructions,
|
||||
attioTriggerOptions,
|
||||
buildAttioExtraFields,
|
||||
buildAttioTriggerSubBlocks,
|
||||
buildRecordOutputs,
|
||||
} from '@/triggers/attio/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
@@ -22,13 +20,18 @@ export const attioRecordCreatedTrigger: TriggerConfig = {
|
||||
version: '1.0.0',
|
||||
icon: AttioIcon,
|
||||
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'attio_record_created',
|
||||
triggerOptions: attioTriggerOptions,
|
||||
includeDropdown: true,
|
||||
setupInstructions: attioSetupInstructions('record.created'),
|
||||
extraFields: buildAttioExtraFields('attio_record_created'),
|
||||
}),
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'selectedTriggerId',
|
||||
title: 'Trigger Type',
|
||||
type: 'dropdown',
|
||||
mode: 'trigger',
|
||||
options: attioTriggerOptions,
|
||||
value: () => 'attio_record_created',
|
||||
required: true,
|
||||
},
|
||||
...buildAttioTriggerSubBlocks('attio_record_created'),
|
||||
],
|
||||
|
||||
outputs: buildRecordOutputs(),
|
||||
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { AttioIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
attioSetupInstructions,
|
||||
attioTriggerOptions,
|
||||
buildAttioExtraFields,
|
||||
buildRecordOutputs,
|
||||
} from '@/triggers/attio/utils'
|
||||
import { buildAttioTriggerSubBlocks, buildRecordOutputs } from '@/triggers/attio/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
/**
|
||||
@@ -21,12 +15,7 @@ export const attioRecordDeletedTrigger: TriggerConfig = {
|
||||
version: '1.0.0',
|
||||
icon: AttioIcon,
|
||||
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'attio_record_deleted',
|
||||
triggerOptions: attioTriggerOptions,
|
||||
setupInstructions: attioSetupInstructions('record.deleted'),
|
||||
extraFields: buildAttioExtraFields('attio_record_deleted'),
|
||||
}),
|
||||
subBlocks: buildAttioTriggerSubBlocks('attio_record_deleted'),
|
||||
|
||||
outputs: buildRecordOutputs(),
|
||||
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { AttioIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
attioSetupInstructions,
|
||||
attioTriggerOptions,
|
||||
buildAttioExtraFields,
|
||||
buildRecordMergedEventOutputs,
|
||||
} from '@/triggers/attio/utils'
|
||||
import { buildAttioTriggerSubBlocks, buildRecordMergedEventOutputs } from '@/triggers/attio/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
/**
|
||||
@@ -21,12 +15,7 @@ export const attioRecordMergedTrigger: TriggerConfig = {
|
||||
version: '1.0.0',
|
||||
icon: AttioIcon,
|
||||
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'attio_record_merged',
|
||||
triggerOptions: attioTriggerOptions,
|
||||
setupInstructions: attioSetupInstructions('record.merged'),
|
||||
extraFields: buildAttioExtraFields('attio_record_merged'),
|
||||
}),
|
||||
subBlocks: buildAttioTriggerSubBlocks('attio_record_merged'),
|
||||
|
||||
outputs: buildRecordMergedEventOutputs(),
|
||||
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { AttioIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
attioSetupInstructions,
|
||||
attioTriggerOptions,
|
||||
buildAttioExtraFields,
|
||||
buildRecordUpdatedOutputs,
|
||||
} from '@/triggers/attio/utils'
|
||||
import { buildAttioTriggerSubBlocks, buildRecordUpdatedOutputs } from '@/triggers/attio/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
/**
|
||||
@@ -21,12 +15,7 @@ export const attioRecordUpdatedTrigger: TriggerConfig = {
|
||||
version: '1.0.0',
|
||||
icon: AttioIcon,
|
||||
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'attio_record_updated',
|
||||
triggerOptions: attioTriggerOptions,
|
||||
setupInstructions: attioSetupInstructions('record.updated'),
|
||||
extraFields: buildAttioExtraFields('attio_record_updated'),
|
||||
}),
|
||||
subBlocks: buildAttioTriggerSubBlocks('attio_record_updated'),
|
||||
|
||||
outputs: buildRecordUpdatedOutputs(),
|
||||
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { AttioIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
attioSetupInstructions,
|
||||
attioTriggerOptions,
|
||||
buildAttioExtraFields,
|
||||
buildTaskOutputs,
|
||||
} from '@/triggers/attio/utils'
|
||||
import { buildAttioTriggerSubBlocks, buildTaskOutputs } from '@/triggers/attio/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
/**
|
||||
@@ -21,12 +15,7 @@ export const attioTaskCreatedTrigger: TriggerConfig = {
|
||||
version: '1.0.0',
|
||||
icon: AttioIcon,
|
||||
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'attio_task_created',
|
||||
triggerOptions: attioTriggerOptions,
|
||||
setupInstructions: attioSetupInstructions('task.created'),
|
||||
extraFields: buildAttioExtraFields('attio_task_created'),
|
||||
}),
|
||||
subBlocks: buildAttioTriggerSubBlocks('attio_task_created'),
|
||||
|
||||
outputs: buildTaskOutputs(),
|
||||
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { AttioIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
attioSetupInstructions,
|
||||
attioTriggerOptions,
|
||||
buildAttioExtraFields,
|
||||
buildTaskOutputs,
|
||||
} from '@/triggers/attio/utils'
|
||||
import { buildAttioTriggerSubBlocks, buildTaskOutputs } from '@/triggers/attio/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
/**
|
||||
@@ -21,12 +15,7 @@ export const attioTaskDeletedTrigger: TriggerConfig = {
|
||||
version: '1.0.0',
|
||||
icon: AttioIcon,
|
||||
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'attio_task_deleted',
|
||||
triggerOptions: attioTriggerOptions,
|
||||
setupInstructions: attioSetupInstructions('task.deleted'),
|
||||
extraFields: buildAttioExtraFields('attio_task_deleted'),
|
||||
}),
|
||||
subBlocks: buildAttioTriggerSubBlocks('attio_task_deleted'),
|
||||
|
||||
outputs: buildTaskOutputs(),
|
||||
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { AttioIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
attioSetupInstructions,
|
||||
attioTriggerOptions,
|
||||
buildAttioExtraFields,
|
||||
buildTaskOutputs,
|
||||
} from '@/triggers/attio/utils'
|
||||
import { buildAttioTriggerSubBlocks, buildTaskOutputs } from '@/triggers/attio/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
/**
|
||||
@@ -21,12 +15,7 @@ export const attioTaskUpdatedTrigger: TriggerConfig = {
|
||||
version: '1.0.0',
|
||||
icon: AttioIcon,
|
||||
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'attio_task_updated',
|
||||
triggerOptions: attioTriggerOptions,
|
||||
setupInstructions: attioSetupInstructions('task.updated'),
|
||||
extraFields: buildAttioExtraFields('attio_task_updated'),
|
||||
}),
|
||||
subBlocks: buildAttioTriggerSubBlocks('attio_task_updated'),
|
||||
|
||||
outputs: buildTaskOutputs(),
|
||||
|
||||
|
||||
@@ -22,15 +22,11 @@ export const attioTriggerOptions = [
|
||||
{ label: 'Generic Webhook (All Events)', id: 'attio_webhook' },
|
||||
]
|
||||
|
||||
export function attioSetupInstructions(eventType: string): string {
|
||||
export function attioSetupInstructions(): string {
|
||||
const instructions = [
|
||||
'<strong>Note:</strong> You need access to the Attio developer settings to create webhooks. See the <a href="https://docs.attio.com/rest-api/guides/webhooks" target="_blank" rel="noopener noreferrer">Attio webhook documentation</a> for details.',
|
||||
'In Attio, navigate to <strong>Settings > Developers</strong> and select your integration.',
|
||||
'Go to the <strong>Webhooks</strong> tab and click <strong>"Create Webhook"</strong>.',
|
||||
'Paste the <strong>Webhook URL</strong> from above into the target URL field.',
|
||||
`Add a subscription with the event type <strong>${eventType}</strong>. You can optionally add filters to scope the events.`,
|
||||
'Save the webhook. Copy the <strong>signing secret</strong> shown and paste it in the field above for signature verification.',
|
||||
'The webhook is now active. Attio will send events to the URL you configured.',
|
||||
'<strong>Note:</strong> Webhooks are automatically created in Attio when you deploy this workflow, and deleted when you undeploy. See the <a href="https://docs.attio.com/rest-api/guides/webhooks" target="_blank" rel="noopener noreferrer">Attio webhook documentation</a> for details.',
|
||||
'Connect your <strong>Attio account</strong> using the credential selector above.',
|
||||
'<strong>Deploy</strong> the workflow — a webhook will be created automatically in your Attio workspace.',
|
||||
]
|
||||
|
||||
return instructions
|
||||
@@ -41,17 +37,34 @@ export function attioSetupInstructions(eventType: string): string {
|
||||
.join('')
|
||||
}
|
||||
|
||||
export function buildAttioExtraFields(triggerId: string): SubBlockConfig[] {
|
||||
/**
|
||||
* Builds subBlocks for an Attio trigger with OAuth credentials and automatic webhook lifecycle.
|
||||
* Used by both the primary trigger (with dropdown) and secondary triggers.
|
||||
*/
|
||||
export function buildAttioTriggerSubBlocks(triggerId: string): SubBlockConfig[] {
|
||||
return [
|
||||
{
|
||||
id: 'webhookSecret',
|
||||
title: 'Webhook Secret',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter the webhook signing secret from Attio',
|
||||
description:
|
||||
'The signing secret from Attio used to verify webhook deliveries via HMAC-SHA256 signature',
|
||||
password: true,
|
||||
required: false,
|
||||
id: 'triggerCredentials',
|
||||
title: 'Attio Account',
|
||||
type: 'oauth-input',
|
||||
serviceId: 'attio',
|
||||
mode: 'trigger',
|
||||
required: true,
|
||||
condition: { field: 'selectedTriggerId', value: triggerId },
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: 'Save',
|
||||
type: 'trigger-save',
|
||||
mode: 'trigger',
|
||||
condition: { field: 'selectedTriggerId', value: triggerId },
|
||||
},
|
||||
{
|
||||
id: 'triggerInstructions',
|
||||
title: 'Setup Instructions',
|
||||
hideFromPreview: true,
|
||||
type: 'text',
|
||||
defaultValue: attioSetupInstructions(),
|
||||
mode: 'trigger',
|
||||
condition: { field: 'selectedTriggerId', value: triggerId },
|
||||
},
|
||||
@@ -257,7 +270,7 @@ export function buildGenericWebhookOutputs(): Record<string, TriggerOutput> {
|
||||
/**
|
||||
* Maps trigger IDs to the exact Attio event type strings.
|
||||
*/
|
||||
const TRIGGER_EVENT_MAP: Record<string, string[]> = {
|
||||
export const TRIGGER_EVENT_MAP: Record<string, string[]> = {
|
||||
attio_record_created: ['record.created'],
|
||||
attio_record_updated: ['record.updated'],
|
||||
attio_record_deleted: ['record.deleted'],
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { AttioIcon } from '@/components/icons'
|
||||
import { buildTriggerSubBlocks } from '@/triggers'
|
||||
import {
|
||||
attioSetupInstructions,
|
||||
attioTriggerOptions,
|
||||
buildAttioExtraFields,
|
||||
buildGenericWebhookOutputs,
|
||||
} from '@/triggers/attio/utils'
|
||||
import { buildAttioTriggerSubBlocks, buildGenericWebhookOutputs } from '@/triggers/attio/utils'
|
||||
import type { TriggerConfig } from '@/triggers/types'
|
||||
|
||||
/**
|
||||
@@ -21,12 +15,7 @@ export const attioWebhookTrigger: TriggerConfig = {
|
||||
version: '1.0.0',
|
||||
icon: AttioIcon,
|
||||
|
||||
subBlocks: buildTriggerSubBlocks({
|
||||
triggerId: 'attio_webhook',
|
||||
triggerOptions: attioTriggerOptions,
|
||||
setupInstructions: attioSetupInstructions('All Events'),
|
||||
extraFields: buildAttioExtraFields('attio_webhook'),
|
||||
}),
|
||||
subBlocks: buildAttioTriggerSubBlocks('attio_webhook'),
|
||||
|
||||
outputs: buildGenericWebhookOutputs(),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user