feat(confluence): add webhook triggers for Confluence events (#3318)

* feat(confluence): add webhook triggers for Confluence events

Adds 16 Confluence triggers: page CRUD, comments, blogs, attachments,
spaces, and labels — plus a generic webhook trigger.

* feat(confluence): wire triggers into block and webhook processor

Add trigger subBlocks and triggers config to ConfluenceV2Block so
triggers appear in the UI. Add Confluence signature verification and
event filtering to the webhook processor.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(confluence): align trigger outputs with actual webhook payloads

- Rewrite output builders to match real Confluence webhook payload
  structure (flat spaceKey, numeric version, actual API fields)
- Remove fabricated fields (nested space/version objects, comment.body)
- Add missing fields (creatorAccountId, lastModifierAccountId, self,
  creationDate, modificationDate, accountType)
- Add extractor functions (extractPageData, extractCommentData, etc.)
  following the same pattern as Jira
- Add formatWebhookInput handler for Confluence in utils.server.ts
  so payloads are properly destructured before reaching workflows
- Make event field matching resilient (check both event and webhookEvent)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(confluence): handle generic webhook in formatWebhookInput

The generic webhook (confluence_webhook) was falling through to
extractPageData, which only returns the page field. For a catch-all
trigger that accepts all event types, preserve all entity fields
(page, comment, blog, attachment, space, label, content).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(confluence): use payload-based filtering instead of nonexistent event field

Confluence Cloud webhooks don't include an event/webhookEvent field in the
body (unlike Jira). Replaced broken event string matching with structural
payload filtering that checks which entity key is present.

* lint

* fix(confluence): read webhookSecret instead of secret in signature verification

* fix(webhooks): read webhookSecret for jira, linear, and github signature verification

These providers define their secret subBlock with id: 'webhookSecret' but the
processor was reading providerConfig.secret which is always undefined, silently
skipping signature verification even when a secret is configured.

* fix(confluence): use event field for exact matching with entity-category fallback

Admin REST API webhooks (Settings > Webhooks) include an event field for
action-level filtering (page_created vs page_updated). Connect app webhooks
omit it, so we fall back to entity-category matching.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Waleed
2026-02-23 23:36:43 -08:00
committed by GitHub
parent 9bd357f184
commit d824ce5b07
22 changed files with 1242 additions and 3 deletions

View File

@@ -3,6 +3,7 @@ import type { BlockConfig } from '@/blocks/types'
import { AuthMode } from '@/blocks/types'
import { normalizeFileInput } from '@/blocks/utils'
import type { ConfluenceResponse } from '@/tools/confluence/types'
import { getTrigger } from '@/triggers'
export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
type: 'confluence',
@@ -838,7 +839,46 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
],
},
},
// Trigger subBlocks
...getTrigger('confluence_page_created').subBlocks,
...getTrigger('confluence_page_updated').subBlocks,
...getTrigger('confluence_page_removed').subBlocks,
...getTrigger('confluence_page_moved').subBlocks,
...getTrigger('confluence_comment_created').subBlocks,
...getTrigger('confluence_comment_removed').subBlocks,
...getTrigger('confluence_blog_created').subBlocks,
...getTrigger('confluence_blog_updated').subBlocks,
...getTrigger('confluence_blog_removed').subBlocks,
...getTrigger('confluence_attachment_created').subBlocks,
...getTrigger('confluence_attachment_removed').subBlocks,
...getTrigger('confluence_space_created').subBlocks,
...getTrigger('confluence_space_updated').subBlocks,
...getTrigger('confluence_label_added').subBlocks,
...getTrigger('confluence_label_removed').subBlocks,
...getTrigger('confluence_webhook').subBlocks,
],
triggers: {
enabled: true,
available: [
'confluence_page_created',
'confluence_page_updated',
'confluence_page_removed',
'confluence_page_moved',
'confluence_comment_created',
'confluence_comment_removed',
'confluence_blog_created',
'confluence_blog_updated',
'confluence_blog_removed',
'confluence_attachment_created',
'confluence_attachment_removed',
'confluence_space_created',
'confluence_space_updated',
'confluence_label_added',
'confluence_label_removed',
'confluence_webhook',
],
},
tools: {
access: [
// Page Tools

View File

@@ -28,6 +28,7 @@ import { getWorkspaceBilledAccountUserId } from '@/lib/workspaces/utils'
import { resolveOAuthAccountId } from '@/app/api/auth/oauth/utils'
import { executeWebhookJob } from '@/background/webhook-execution'
import { resolveEnvVarReferences } from '@/executor/utils/reference-validation'
import { isConfluencePayloadMatch } from '@/triggers/confluence/utils'
import { isGitHubEventMatch } from '@/triggers/github/utils'
import { isHubSpotContactEventMatch } from '@/triggers/hubspot/utils'
import { isJiraEventMatch } from '@/triggers/jira/utils'
@@ -608,7 +609,7 @@ export async function verifyProviderAuth(
}
if (foundWebhook.provider === 'linear') {
const secret = providerConfig.secret as string | undefined
const secret = providerConfig.webhookSecret as string | undefined
if (secret) {
const signature = request.headers.get('Linear-Signature')
@@ -683,7 +684,7 @@ export async function verifyProviderAuth(
}
if (foundWebhook.provider === 'jira') {
const secret = providerConfig.secret as string | undefined
const secret = providerConfig.webhookSecret as string | undefined
if (secret) {
const signature = request.headers.get('X-Hub-Signature')
@@ -707,8 +708,33 @@ export async function verifyProviderAuth(
}
}
if (foundWebhook.provider === 'confluence') {
const secret = providerConfig.webhookSecret as string | undefined
if (secret) {
const signature = request.headers.get('X-Hub-Signature')
if (!signature) {
logger.warn(`[${requestId}] Confluence webhook missing signature header`)
return new NextResponse('Unauthorized - Missing Confluence signature', { status: 401 })
}
const isValidSignature = validateJiraSignature(secret, signature, rawBody)
if (!isValidSignature) {
logger.warn(`[${requestId}] Confluence signature verification failed`, {
signatureLength: signature.length,
secretLength: secret.length,
})
return new NextResponse('Unauthorized - Invalid Confluence signature', { status: 401 })
}
logger.debug(`[${requestId}] Confluence signature verified successfully`)
}
}
if (foundWebhook.provider === 'github') {
const secret = providerConfig.secret as string | undefined
const secret = providerConfig.webhookSecret as string | undefined
if (secret) {
// GitHub supports both SHA-256 (preferred) and SHA-1 (legacy)
@@ -930,6 +956,27 @@ export async function queueWebhookExecution(
}
}
if (foundWebhook.provider === 'confluence') {
const providerConfig = (foundWebhook.providerConfig as Record<string, any>) || {}
const triggerId = providerConfig.triggerId as string | undefined
if (triggerId && !isConfluencePayloadMatch(triggerId, body)) {
logger.debug(
`[${options.requestId}] Confluence payload mismatch for trigger ${triggerId}. Skipping execution.`,
{
webhookId: foundWebhook.id,
workflowId: foundWorkflow.id,
triggerId,
bodyKeys: Object.keys(body),
}
)
return NextResponse.json({
message: 'Payload does not match trigger configuration. Ignoring.',
})
}
}
if (foundWebhook.provider === 'hubspot') {
const providerConfig = (foundWebhook.providerConfig as Record<string, any>) || {}
const triggerId = providerConfig.triggerId as string | undefined

View File

@@ -1197,6 +1197,53 @@ export async function formatWebhookInput(
return extractIssueData(body)
}
if (foundWebhook.provider === 'confluence') {
const {
extractPageData,
extractCommentData,
extractBlogData,
extractAttachmentData,
extractSpaceData,
extractLabelData,
} = await import('@/triggers/confluence/utils')
const providerConfig = (foundWebhook.providerConfig as Record<string, any>) || {}
const triggerId = providerConfig.triggerId as string | undefined
if (triggerId?.startsWith('confluence_comment_')) {
return extractCommentData(body)
}
if (triggerId?.startsWith('confluence_blog_')) {
return extractBlogData(body)
}
if (triggerId?.startsWith('confluence_attachment_')) {
return extractAttachmentData(body)
}
if (triggerId?.startsWith('confluence_space_')) {
return extractSpaceData(body)
}
if (triggerId?.startsWith('confluence_label_')) {
return extractLabelData(body)
}
// Generic webhook — preserve all entity fields since event type varies
if (triggerId === 'confluence_webhook') {
return {
timestamp: body.timestamp,
userAccountId: body.userAccountId,
accountType: body.accountType,
page: body.page || null,
comment: body.comment || null,
blog: body.blog || body.blogpost || null,
attachment: body.attachment || null,
space: body.space || null,
label: body.label || null,
content: body.content || null,
}
}
// Default: page events
return extractPageData(body)
}
if (foundWebhook.provider === 'stripe') {
return body
}

View File

@@ -0,0 +1,41 @@
import { ConfluenceIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
buildAttachmentOutputs,
buildConfluenceAttachmentExtraFields,
confluenceSetupInstructions,
confluenceTriggerOptions,
} from '@/triggers/confluence/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Confluence Attachment Created Trigger
*
* Triggers when a new attachment is uploaded to a page or blog post in Confluence.
*/
export const confluenceAttachmentCreatedTrigger: TriggerConfig = {
id: 'confluence_attachment_created',
name: 'Confluence Attachment Created',
provider: 'confluence',
description: 'Trigger workflow when an attachment is uploaded in Confluence',
version: '1.0.0',
icon: ConfluenceIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'confluence_attachment_created',
triggerOptions: confluenceTriggerOptions,
setupInstructions: confluenceSetupInstructions('attachment_created'),
extraFields: buildConfluenceAttachmentExtraFields('confluence_attachment_created'),
}),
outputs: buildAttachmentOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Hub-Signature': 'sha256=...',
'X-Atlassian-Webhook-Identifier': 'unique-webhook-id',
},
},
}

View File

@@ -0,0 +1,41 @@
import { ConfluenceIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
buildAttachmentOutputs,
buildConfluenceAttachmentExtraFields,
confluenceSetupInstructions,
confluenceTriggerOptions,
} from '@/triggers/confluence/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Confluence Attachment Removed Trigger
*
* Triggers when an attachment is removed or trashed from a page or blog post in Confluence.
*/
export const confluenceAttachmentRemovedTrigger: TriggerConfig = {
id: 'confluence_attachment_removed',
name: 'Confluence Attachment Removed',
provider: 'confluence',
description: 'Trigger workflow when an attachment is removed in Confluence',
version: '1.0.0',
icon: ConfluenceIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'confluence_attachment_removed',
triggerOptions: confluenceTriggerOptions,
setupInstructions: confluenceSetupInstructions('attachment_removed'),
extraFields: buildConfluenceAttachmentExtraFields('confluence_attachment_removed'),
}),
outputs: buildAttachmentOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Hub-Signature': 'sha256=...',
'X-Atlassian-Webhook-Identifier': 'unique-webhook-id',
},
},
}

View File

@@ -0,0 +1,41 @@
import { ConfluenceIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
buildBlogOutputs,
buildConfluenceExtraFields,
confluenceSetupInstructions,
confluenceTriggerOptions,
} from '@/triggers/confluence/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Confluence Blog Post Created Trigger
*
* Triggers when a new blog post is created in Confluence.
*/
export const confluenceBlogCreatedTrigger: TriggerConfig = {
id: 'confluence_blog_created',
name: 'Confluence Blog Post Created',
provider: 'confluence',
description: 'Trigger workflow when a blog post is created in Confluence',
version: '1.0.0',
icon: ConfluenceIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'confluence_blog_created',
triggerOptions: confluenceTriggerOptions,
setupInstructions: confluenceSetupInstructions('blog_created'),
extraFields: buildConfluenceExtraFields('confluence_blog_created'),
}),
outputs: buildBlogOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Hub-Signature': 'sha256=...',
'X-Atlassian-Webhook-Identifier': 'unique-webhook-id',
},
},
}

View File

@@ -0,0 +1,41 @@
import { ConfluenceIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
buildBlogOutputs,
buildConfluenceExtraFields,
confluenceSetupInstructions,
confluenceTriggerOptions,
} from '@/triggers/confluence/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Confluence Blog Post Removed Trigger
*
* Triggers when a blog post is removed or trashed in Confluence.
*/
export const confluenceBlogRemovedTrigger: TriggerConfig = {
id: 'confluence_blog_removed',
name: 'Confluence Blog Post Removed',
provider: 'confluence',
description: 'Trigger workflow when a blog post is removed in Confluence',
version: '1.0.0',
icon: ConfluenceIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'confluence_blog_removed',
triggerOptions: confluenceTriggerOptions,
setupInstructions: confluenceSetupInstructions('blog_removed'),
extraFields: buildConfluenceExtraFields('confluence_blog_removed'),
}),
outputs: buildBlogOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Hub-Signature': 'sha256=...',
'X-Atlassian-Webhook-Identifier': 'unique-webhook-id',
},
},
}

View File

@@ -0,0 +1,41 @@
import { ConfluenceIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
buildBlogOutputs,
buildConfluenceExtraFields,
confluenceSetupInstructions,
confluenceTriggerOptions,
} from '@/triggers/confluence/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Confluence Blog Post Updated Trigger
*
* Triggers when a blog post is updated in Confluence.
*/
export const confluenceBlogUpdatedTrigger: TriggerConfig = {
id: 'confluence_blog_updated',
name: 'Confluence Blog Post Updated',
provider: 'confluence',
description: 'Trigger workflow when a blog post is updated in Confluence',
version: '1.0.0',
icon: ConfluenceIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'confluence_blog_updated',
triggerOptions: confluenceTriggerOptions,
setupInstructions: confluenceSetupInstructions('blog_updated'),
extraFields: buildConfluenceExtraFields('confluence_blog_updated'),
}),
outputs: buildBlogOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Hub-Signature': 'sha256=...',
'X-Atlassian-Webhook-Identifier': 'unique-webhook-id',
},
},
}

View File

@@ -0,0 +1,41 @@
import { ConfluenceIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
buildCommentOutputs,
buildConfluenceExtraFields,
confluenceSetupInstructions,
confluenceTriggerOptions,
} from '@/triggers/confluence/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Confluence Comment Created Trigger
*
* Triggers when a new comment is created on a page or blog post in Confluence.
*/
export const confluenceCommentCreatedTrigger: TriggerConfig = {
id: 'confluence_comment_created',
name: 'Confluence Comment Created',
provider: 'confluence',
description: 'Trigger workflow when a comment is created in Confluence',
version: '1.0.0',
icon: ConfluenceIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'confluence_comment_created',
triggerOptions: confluenceTriggerOptions,
setupInstructions: confluenceSetupInstructions('comment_created'),
extraFields: buildConfluenceExtraFields('confluence_comment_created'),
}),
outputs: buildCommentOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Hub-Signature': 'sha256=...',
'X-Atlassian-Webhook-Identifier': 'unique-webhook-id',
},
},
}

View File

@@ -0,0 +1,41 @@
import { ConfluenceIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
buildCommentOutputs,
buildConfluenceExtraFields,
confluenceSetupInstructions,
confluenceTriggerOptions,
} from '@/triggers/confluence/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Confluence Comment Removed Trigger
*
* Triggers when a comment is removed from a page or blog post in Confluence.
*/
export const confluenceCommentRemovedTrigger: TriggerConfig = {
id: 'confluence_comment_removed',
name: 'Confluence Comment Removed',
provider: 'confluence',
description: 'Trigger workflow when a comment is removed in Confluence',
version: '1.0.0',
icon: ConfluenceIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'confluence_comment_removed',
triggerOptions: confluenceTriggerOptions,
setupInstructions: confluenceSetupInstructions('comment_removed'),
extraFields: buildConfluenceExtraFields('confluence_comment_removed'),
}),
outputs: buildCommentOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Hub-Signature': 'sha256=...',
'X-Atlassian-Webhook-Identifier': 'unique-webhook-id',
},
},
}

View File

@@ -0,0 +1,21 @@
/**
* Confluence Triggers
* Export all Confluence webhook triggers
*/
export { confluenceAttachmentCreatedTrigger } from './attachment_created'
export { confluenceAttachmentRemovedTrigger } from './attachment_removed'
export { confluenceBlogCreatedTrigger } from './blog_created'
export { confluenceBlogRemovedTrigger } from './blog_removed'
export { confluenceBlogUpdatedTrigger } from './blog_updated'
export { confluenceCommentCreatedTrigger } from './comment_created'
export { confluenceCommentRemovedTrigger } from './comment_removed'
export { confluenceLabelAddedTrigger } from './label_added'
export { confluenceLabelRemovedTrigger } from './label_removed'
export { confluencePageCreatedTrigger } from './page_created'
export { confluencePageMovedTrigger } from './page_moved'
export { confluencePageRemovedTrigger } from './page_removed'
export { confluencePageUpdatedTrigger } from './page_updated'
export { confluenceSpaceCreatedTrigger } from './space_created'
export { confluenceSpaceUpdatedTrigger } from './space_updated'
export { confluenceWebhookTrigger } from './webhook'

View File

@@ -0,0 +1,41 @@
import { ConfluenceIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
buildConfluenceExtraFields,
buildLabelOutputs,
confluenceSetupInstructions,
confluenceTriggerOptions,
} from '@/triggers/confluence/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Confluence Label Added Trigger
*
* Triggers when a label is added to a page, blog post, or other content in Confluence.
*/
export const confluenceLabelAddedTrigger: TriggerConfig = {
id: 'confluence_label_added',
name: 'Confluence Label Added',
provider: 'confluence',
description: 'Trigger workflow when a label is added to content in Confluence',
version: '1.0.0',
icon: ConfluenceIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'confluence_label_added',
triggerOptions: confluenceTriggerOptions,
setupInstructions: confluenceSetupInstructions('label_added'),
extraFields: buildConfluenceExtraFields('confluence_label_added'),
}),
outputs: buildLabelOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Hub-Signature': 'sha256=...',
'X-Atlassian-Webhook-Identifier': 'unique-webhook-id',
},
},
}

View File

@@ -0,0 +1,41 @@
import { ConfluenceIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
buildConfluenceExtraFields,
buildLabelOutputs,
confluenceSetupInstructions,
confluenceTriggerOptions,
} from '@/triggers/confluence/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Confluence Label Removed Trigger
*
* Triggers when a label is removed from a page, blog post, or other content in Confluence.
*/
export const confluenceLabelRemovedTrigger: TriggerConfig = {
id: 'confluence_label_removed',
name: 'Confluence Label Removed',
provider: 'confluence',
description: 'Trigger workflow when a label is removed from content in Confluence',
version: '1.0.0',
icon: ConfluenceIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'confluence_label_removed',
triggerOptions: confluenceTriggerOptions,
setupInstructions: confluenceSetupInstructions('label_removed'),
extraFields: buildConfluenceExtraFields('confluence_label_removed'),
}),
outputs: buildLabelOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Hub-Signature': 'sha256=...',
'X-Atlassian-Webhook-Identifier': 'unique-webhook-id',
},
},
}

View File

@@ -0,0 +1,43 @@
import { ConfluenceIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
buildConfluenceExtraFields,
buildPageOutputs,
confluenceSetupInstructions,
confluenceTriggerOptions,
} from '@/triggers/confluence/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Confluence Page Created Trigger
*
* This is the PRIMARY trigger - it includes the dropdown for selecting trigger type.
* Triggers when a new page is created in Confluence.
*/
export const confluencePageCreatedTrigger: TriggerConfig = {
id: 'confluence_page_created',
name: 'Confluence Page Created',
provider: 'confluence',
description: 'Trigger workflow when a new page is created in Confluence',
version: '1.0.0',
icon: ConfluenceIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'confluence_page_created',
triggerOptions: confluenceTriggerOptions,
includeDropdown: true,
setupInstructions: confluenceSetupInstructions('page_created'),
extraFields: buildConfluenceExtraFields('confluence_page_created'),
}),
outputs: buildPageOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Hub-Signature': 'sha256=...',
'X-Atlassian-Webhook-Identifier': 'unique-webhook-id',
},
},
}

View File

@@ -0,0 +1,41 @@
import { ConfluenceIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
buildConfluenceExtraFields,
buildPageOutputs,
confluenceSetupInstructions,
confluenceTriggerOptions,
} from '@/triggers/confluence/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Confluence Page Moved Trigger
*
* Triggers when a page is moved to a different space or parent in Confluence.
*/
export const confluencePageMovedTrigger: TriggerConfig = {
id: 'confluence_page_moved',
name: 'Confluence Page Moved',
provider: 'confluence',
description: 'Trigger workflow when a page is moved in Confluence',
version: '1.0.0',
icon: ConfluenceIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'confluence_page_moved',
triggerOptions: confluenceTriggerOptions,
setupInstructions: confluenceSetupInstructions('page_moved'),
extraFields: buildConfluenceExtraFields('confluence_page_moved'),
}),
outputs: buildPageOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Hub-Signature': 'sha256=...',
'X-Atlassian-Webhook-Identifier': 'unique-webhook-id',
},
},
}

View File

@@ -0,0 +1,41 @@
import { ConfluenceIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
buildConfluenceExtraFields,
buildPageOutputs,
confluenceSetupInstructions,
confluenceTriggerOptions,
} from '@/triggers/confluence/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Confluence Page Removed Trigger
*
* Triggers when a page is removed or trashed in Confluence.
*/
export const confluencePageRemovedTrigger: TriggerConfig = {
id: 'confluence_page_removed',
name: 'Confluence Page Removed',
provider: 'confluence',
description: 'Trigger workflow when a page is removed or trashed in Confluence',
version: '1.0.0',
icon: ConfluenceIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'confluence_page_removed',
triggerOptions: confluenceTriggerOptions,
setupInstructions: confluenceSetupInstructions('page_removed'),
extraFields: buildConfluenceExtraFields('confluence_page_removed'),
}),
outputs: buildPageOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Hub-Signature': 'sha256=...',
'X-Atlassian-Webhook-Identifier': 'unique-webhook-id',
},
},
}

View File

@@ -0,0 +1,41 @@
import { ConfluenceIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
buildConfluenceExtraFields,
buildPageOutputs,
confluenceSetupInstructions,
confluenceTriggerOptions,
} from '@/triggers/confluence/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Confluence Page Updated Trigger
*
* Triggers when an existing page is updated in Confluence.
*/
export const confluencePageUpdatedTrigger: TriggerConfig = {
id: 'confluence_page_updated',
name: 'Confluence Page Updated',
provider: 'confluence',
description: 'Trigger workflow when a page is updated in Confluence',
version: '1.0.0',
icon: ConfluenceIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'confluence_page_updated',
triggerOptions: confluenceTriggerOptions,
setupInstructions: confluenceSetupInstructions('page_updated'),
extraFields: buildConfluenceExtraFields('confluence_page_updated'),
}),
outputs: buildPageOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Hub-Signature': 'sha256=...',
'X-Atlassian-Webhook-Identifier': 'unique-webhook-id',
},
},
}

View File

@@ -0,0 +1,41 @@
import { ConfluenceIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
buildConfluenceExtraFields,
buildSpaceOutputs,
confluenceSetupInstructions,
confluenceTriggerOptions,
} from '@/triggers/confluence/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Confluence Space Created Trigger
*
* Triggers when a new space is created in Confluence.
*/
export const confluenceSpaceCreatedTrigger: TriggerConfig = {
id: 'confluence_space_created',
name: 'Confluence Space Created',
provider: 'confluence',
description: 'Trigger workflow when a new space is created in Confluence',
version: '1.0.0',
icon: ConfluenceIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'confluence_space_created',
triggerOptions: confluenceTriggerOptions,
setupInstructions: confluenceSetupInstructions('space_created'),
extraFields: buildConfluenceExtraFields('confluence_space_created'),
}),
outputs: buildSpaceOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Hub-Signature': 'sha256=...',
'X-Atlassian-Webhook-Identifier': 'unique-webhook-id',
},
},
}

View File

@@ -0,0 +1,41 @@
import { ConfluenceIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
buildConfluenceExtraFields,
buildSpaceOutputs,
confluenceSetupInstructions,
confluenceTriggerOptions,
} from '@/triggers/confluence/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Confluence Space Updated Trigger
*
* Triggers when a space is updated (settings, permissions, etc.) in Confluence.
*/
export const confluenceSpaceUpdatedTrigger: TriggerConfig = {
id: 'confluence_space_updated',
name: 'Confluence Space Updated',
provider: 'confluence',
description: 'Trigger workflow when a space is updated in Confluence',
version: '1.0.0',
icon: ConfluenceIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'confluence_space_updated',
triggerOptions: confluenceTriggerOptions,
setupInstructions: confluenceSetupInstructions('space_updated'),
extraFields: buildConfluenceExtraFields('confluence_space_updated'),
}),
outputs: buildSpaceOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Hub-Signature': 'sha256=...',
'X-Atlassian-Webhook-Identifier': 'unique-webhook-id',
},
},
}

View File

@@ -0,0 +1,392 @@
import type { SubBlockConfig } from '@/blocks/types'
import type { TriggerOutput } from '@/triggers/types'
export const confluenceTriggerOptions = [
{ label: 'Page Created', id: 'confluence_page_created' },
{ label: 'Page Updated', id: 'confluence_page_updated' },
{ label: 'Page Removed', id: 'confluence_page_removed' },
{ label: 'Page Moved', id: 'confluence_page_moved' },
{ label: 'Comment Created', id: 'confluence_comment_created' },
{ label: 'Comment Removed', id: 'confluence_comment_removed' },
{ label: 'Blog Post Created', id: 'confluence_blog_created' },
{ label: 'Blog Post Updated', id: 'confluence_blog_updated' },
{ label: 'Blog Post Removed', id: 'confluence_blog_removed' },
{ label: 'Attachment Created', id: 'confluence_attachment_created' },
{ label: 'Attachment Removed', id: 'confluence_attachment_removed' },
{ label: 'Space Created', id: 'confluence_space_created' },
{ label: 'Space Updated', id: 'confluence_space_updated' },
{ label: 'Label Added', id: 'confluence_label_added' },
{ label: 'Label Removed', id: 'confluence_label_removed' },
{ label: 'Generic Webhook (All Events)', id: 'confluence_webhook' },
]
export function confluenceSetupInstructions(eventType: string): string {
const instructions = [
'<strong>Note:</strong> You must have admin permissions in your Confluence workspace to create webhooks. See the <a href="https://developer.atlassian.com/cloud/confluence/modules/webhook/" target="_blank" rel="noopener noreferrer">Confluence webhook documentation</a> for details.',
'In Confluence, navigate to <strong>Settings > Webhooks</strong>.',
'Click <strong>"Create a Webhook"</strong> to add a new webhook.',
'Paste the <strong>Webhook URL</strong> from above into the URL field.',
'Optionally, enter the <strong>Webhook Secret</strong> from above into the secret field for added security.',
`Select the events you want to trigger this workflow. For this trigger, select <strong>${eventType}</strong>.`,
'Click <strong>"Create"</strong> to activate the webhook.',
]
return instructions
.map(
(instruction, index) =>
`<div class="mb-3">${index === 0 ? instruction : `<strong>${index}.</strong> ${instruction}`}</div>`
)
.join('')
}
export function buildConfluenceExtraFields(triggerId: string): SubBlockConfig[] {
return [
{
id: 'webhookSecret',
title: 'Webhook Secret',
type: 'short-input',
placeholder: 'Enter a strong secret',
description:
'Optional secret to validate webhook deliveries from Confluence using HMAC signature',
password: true,
required: false,
mode: 'trigger',
condition: { field: 'selectedTriggerId', value: triggerId },
},
{
id: 'confluenceDomain',
title: 'Confluence Domain',
type: 'short-input',
placeholder: 'your-company.atlassian.net',
description: 'Your Confluence Cloud domain',
required: false,
mode: 'trigger',
condition: { field: 'selectedTriggerId', value: triggerId },
},
]
}
export function buildConfluenceAttachmentExtraFields(triggerId: string): SubBlockConfig[] {
return [
...buildConfluenceExtraFields(triggerId),
{
id: 'confluenceEmail',
title: 'Confluence Email',
type: 'short-input',
placeholder: 'user@example.com',
description:
'Your Atlassian account email. Required together with API token to download attachment files.',
required: false,
mode: 'trigger',
condition: { field: 'selectedTriggerId', value: triggerId },
},
{
id: 'confluenceApiToken',
title: 'API Token',
type: 'short-input',
placeholder: 'Enter your Atlassian API token',
description:
'API token from https://id.atlassian.com/manage-profile/security/api-tokens. Required to download attachment file content.',
password: true,
required: false,
mode: 'trigger',
condition: { field: 'selectedTriggerId', value: triggerId },
},
{
id: 'includeFileContent',
title: 'Include File Content',
type: 'switch',
defaultValue: false,
description:
'Download and include actual file content from attachments. Requires email, API token, and domain.',
required: false,
mode: 'trigger',
condition: { field: 'selectedTriggerId', value: triggerId },
},
]
}
/**
* Base webhook outputs common to all Confluence triggers.
*/
function buildBaseWebhookOutputs(): Record<string, TriggerOutput> {
return {
timestamp: {
type: 'number',
description: 'Timestamp of the webhook event (Unix epoch milliseconds)',
},
userAccountId: {
type: 'string',
description: 'Account ID of the user who triggered the event',
},
accountType: {
type: 'string',
description: 'Account type (e.g., customer)',
},
}
}
/**
* Shared content-entity output fields present on page, blog, comment, and attachment objects.
*/
function buildContentEntityFields(): Record<string, TriggerOutput> {
return {
id: { type: 'number', description: 'Content ID' },
title: { type: 'string', description: 'Content title' },
contentType: {
type: 'string',
description: 'Content type (page, blogpost, comment, attachment)',
},
version: { type: 'number', description: 'Version number' },
spaceKey: { type: 'string', description: 'Space key the content belongs to' },
creatorAccountId: { type: 'string', description: 'Account ID of the creator' },
lastModifierAccountId: { type: 'string', description: 'Account ID of the last modifier' },
self: { type: 'string', description: 'URL link to the content' },
creationDate: { type: 'number', description: 'Creation timestamp (Unix epoch milliseconds)' },
modificationDate: {
type: 'number',
description: 'Last modification timestamp (Unix epoch milliseconds)',
},
}
}
/** Page-related outputs for page events. */
export function buildPageOutputs(): Record<string, TriggerOutput> {
return {
...buildBaseWebhookOutputs(),
page: buildContentEntityFields(),
}
}
/** Comment-related outputs for comment events. */
export function buildCommentOutputs(): Record<string, TriggerOutput> {
return {
...buildBaseWebhookOutputs(),
comment: {
...buildContentEntityFields(),
parent: {
id: { type: 'number', description: 'Parent page/blog ID' },
title: { type: 'string', description: 'Parent page/blog title' },
contentType: { type: 'string', description: 'Parent content type (page or blogpost)' },
spaceKey: { type: 'string', description: 'Space key of the parent' },
self: { type: 'string', description: 'URL link to the parent content' },
},
},
}
}
/** Blog post outputs for blog events. */
export function buildBlogOutputs(): Record<string, TriggerOutput> {
return {
...buildBaseWebhookOutputs(),
blog: buildContentEntityFields(),
}
}
/** Attachment-related outputs for attachment events. */
export function buildAttachmentOutputs(): Record<string, TriggerOutput> {
return {
...buildBaseWebhookOutputs(),
attachment: {
...buildContentEntityFields(),
mediaType: { type: 'string', description: 'MIME type of the attachment' },
fileSize: { type: 'number', description: 'File size in bytes' },
parent: {
id: { type: 'number', description: 'Container page/blog ID' },
title: { type: 'string', description: 'Container page/blog title' },
contentType: { type: 'string', description: 'Container content type' },
},
},
files: {
type: 'file[]',
description:
'Attachment file content downloaded from Confluence (if includeFileContent is enabled with credentials)',
},
}
}
/** Space-related outputs for space events. */
export function buildSpaceOutputs(): Record<string, TriggerOutput> {
return {
...buildBaseWebhookOutputs(),
space: {
key: { type: 'string', description: 'Space key' },
name: { type: 'string', description: 'Space name' },
self: { type: 'string', description: 'URL link to the space' },
},
}
}
/** Label-related outputs for label events. */
export function buildLabelOutputs(): Record<string, TriggerOutput> {
return {
...buildBaseWebhookOutputs(),
label: {
name: { type: 'string', description: 'Label name' },
id: { type: 'string', description: 'Label ID' },
prefix: { type: 'string', description: 'Label prefix (global, my, team)' },
},
content: {
id: { type: 'number', description: 'Content ID the label was added to or removed from' },
title: { type: 'string', description: 'Content title' },
contentType: { type: 'string', description: 'Content type (page, blogpost)' },
},
}
}
/** Combined outputs for the generic webhook trigger (all events). */
export function buildGenericWebhookOutputs(): Record<string, TriggerOutput> {
return {
...buildBaseWebhookOutputs(),
page: { type: 'json', description: 'Page object (present in page events)' },
comment: { type: 'json', description: 'Comment object (present in comment events)' },
blog: { type: 'json', description: 'Blog post object (present in blog events)' },
attachment: { type: 'json', description: 'Attachment object (present in attachment events)' },
space: { type: 'json', description: 'Space object (present in space events)' },
label: { type: 'json', description: 'Label object (present in label events)' },
content: { type: 'json', description: 'Content object (present in label events)' },
files: {
type: 'file[]',
description:
'Attachment file content (present in attachment events when includeFileContent is enabled)',
},
}
}
export function extractPageData(body: any) {
return {
timestamp: body.timestamp,
userAccountId: body.userAccountId,
accountType: body.accountType,
page: body.page || {},
}
}
export function extractCommentData(body: any) {
return {
timestamp: body.timestamp,
userAccountId: body.userAccountId,
accountType: body.accountType,
comment: body.comment || {},
}
}
export function extractBlogData(body: any) {
return {
timestamp: body.timestamp,
userAccountId: body.userAccountId,
accountType: body.accountType,
blog: body.blog || body.blogpost || {},
}
}
export function extractAttachmentData(body: any) {
return {
timestamp: body.timestamp,
userAccountId: body.userAccountId,
accountType: body.accountType,
attachment: body.attachment || {},
}
}
export function extractSpaceData(body: any) {
return {
timestamp: body.timestamp,
userAccountId: body.userAccountId,
accountType: body.accountType,
space: body.space || {},
}
}
export function extractLabelData(body: any) {
return {
timestamp: body.timestamp,
userAccountId: body.userAccountId,
accountType: body.accountType,
label: body.label || {},
content: body.content || body.page || body.blog || {},
}
}
/**
* Maps trigger IDs to the exact Confluence event strings they accept.
* Admin REST API webhooks include an `event` field (e.g. `"event": "page_created"`).
* Connect app webhooks do NOT — for those we fall back to entity-category matching.
*/
const TRIGGER_EVENT_MAP: Record<string, string[]> = {
confluence_page_created: ['page_created'],
confluence_page_updated: ['page_updated'],
confluence_page_removed: ['page_removed', 'page_trashed'],
confluence_page_moved: ['page_moved'],
confluence_comment_created: ['comment_created'],
confluence_comment_removed: ['comment_removed'],
confluence_blog_created: ['blog_created'],
confluence_blog_updated: ['blog_updated'],
confluence_blog_removed: ['blog_removed', 'blog_trashed'],
confluence_attachment_created: ['attachment_created'],
confluence_attachment_removed: ['attachment_removed', 'attachment_trashed'],
confluence_space_created: ['space_created'],
confluence_space_updated: ['space_updated'],
confluence_label_added: ['label_added', 'label_created'],
confluence_label_removed: ['label_removed', 'label_deleted'],
}
const TRIGGER_CATEGORY_MAP: Record<string, string> = {
confluence_page_created: 'page',
confluence_page_updated: 'page',
confluence_page_removed: 'page',
confluence_page_moved: 'page',
confluence_comment_created: 'comment',
confluence_comment_removed: 'comment',
confluence_blog_created: 'blog',
confluence_blog_updated: 'blog',
confluence_blog_removed: 'blog',
confluence_attachment_created: 'attachment',
confluence_attachment_removed: 'attachment',
confluence_space_created: 'space',
confluence_space_updated: 'space',
confluence_label_added: 'label',
confluence_label_removed: 'label',
}
/**
* Infers the entity category from a Confluence webhook payload by checking
* which entity key is present in the body.
*/
function inferEntityCategory(body: Record<string, unknown>): string | null {
if (body.comment) return 'comment'
if (body.attachment) return 'attachment'
if (body.blog || body.blogpost) return 'blog'
if (body.label) return 'label'
if (body.page) return 'page'
if (body.space) return 'space'
return null
}
/**
* Checks if a Confluence webhook payload matches a trigger.
*
* Admin REST API webhooks (Settings > Webhooks) include an `event` field
* for exact action-level matching. Connect app webhooks omit it, so we
* fall back to entity-category matching (page vs comment vs blog, etc.).
*/
export function isConfluencePayloadMatch(
triggerId: string,
body: Record<string, unknown>
): boolean {
if (triggerId === 'confluence_webhook') {
return true
}
const event = body.event as string | undefined
if (event) {
const acceptedEvents = TRIGGER_EVENT_MAP[triggerId]
return acceptedEvents ? acceptedEvents.includes(event) : false
}
const expectedCategory = TRIGGER_CATEGORY_MAP[triggerId]
if (!expectedCategory) {
return false
}
return inferEntityCategory(body) === expectedCategory
}

View File

@@ -0,0 +1,41 @@
import { ConfluenceIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import {
buildConfluenceAttachmentExtraFields,
buildGenericWebhookOutputs,
confluenceSetupInstructions,
confluenceTriggerOptions,
} from '@/triggers/confluence/utils'
import type { TriggerConfig } from '@/triggers/types'
/**
* Generic Confluence Webhook Trigger
*
* Captures all Confluence webhook events without filtering.
*/
export const confluenceWebhookTrigger: TriggerConfig = {
id: 'confluence_webhook',
name: 'Confluence Webhook (All Events)',
provider: 'confluence',
description: 'Trigger workflow on any Confluence webhook event',
version: '1.0.0',
icon: ConfluenceIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'confluence_webhook',
triggerOptions: confluenceTriggerOptions,
setupInstructions: confluenceSetupInstructions('All Events'),
extraFields: buildConfluenceAttachmentExtraFields('confluence_webhook'),
}),
outputs: buildGenericWebhookOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Hub-Signature': 'sha256=...',
'X-Atlassian-Webhook-Identifier': 'unique-webhook-id',
},
},
}

View File

@@ -21,6 +21,24 @@ import {
circlebackMeetingNotesTrigger,
circlebackWebhookTrigger,
} from '@/triggers/circleback'
import {
confluenceAttachmentCreatedTrigger,
confluenceAttachmentRemovedTrigger,
confluenceBlogCreatedTrigger,
confluenceBlogRemovedTrigger,
confluenceBlogUpdatedTrigger,
confluenceCommentCreatedTrigger,
confluenceCommentRemovedTrigger,
confluenceLabelAddedTrigger,
confluenceLabelRemovedTrigger,
confluencePageCreatedTrigger,
confluencePageMovedTrigger,
confluencePageRemovedTrigger,
confluencePageUpdatedTrigger,
confluenceSpaceCreatedTrigger,
confluenceSpaceUpdatedTrigger,
confluenceWebhookTrigger,
} from '@/triggers/confluence'
import { firefliesTranscriptionCompleteTrigger } from '@/triggers/fireflies'
import { genericWebhookTrigger } from '@/triggers/generic'
import {
@@ -140,6 +158,22 @@ export const TRIGGER_REGISTRY: TriggerRegistry = {
calcom_meeting_ended: calcomMeetingEndedTrigger,
calcom_recording_ready: calcomRecordingReadyTrigger,
calcom_webhook: calcomWebhookTrigger,
confluence_webhook: confluenceWebhookTrigger,
confluence_page_created: confluencePageCreatedTrigger,
confluence_page_updated: confluencePageUpdatedTrigger,
confluence_page_removed: confluencePageRemovedTrigger,
confluence_page_moved: confluencePageMovedTrigger,
confluence_comment_created: confluenceCommentCreatedTrigger,
confluence_comment_removed: confluenceCommentRemovedTrigger,
confluence_blog_created: confluenceBlogCreatedTrigger,
confluence_blog_updated: confluenceBlogUpdatedTrigger,
confluence_blog_removed: confluenceBlogRemovedTrigger,
confluence_attachment_created: confluenceAttachmentCreatedTrigger,
confluence_attachment_removed: confluenceAttachmentRemovedTrigger,
confluence_space_created: confluenceSpaceCreatedTrigger,
confluence_space_updated: confluenceSpaceUpdatedTrigger,
confluence_label_added: confluenceLabelAddedTrigger,
confluence_label_removed: confluenceLabelRemovedTrigger,
generic_webhook: genericWebhookTrigger,
github_webhook: githubWebhookTrigger,
github_issue_opened: githubIssueOpenedTrigger,