From 134d76d0b1e81328d389e0f59ab844f72f38e02a Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 25 Feb 2026 13:56:48 -0800 Subject: [PATCH 01/16] feat(confluence): add get user by account ID tool --- apps/docs/components/ui/icon-mapping.ts | 12 +- .../docs/content/docs/en/tools/confluence.mdx | 24 ++++ apps/docs/content/docs/en/tools/meta.json | 2 +- .../app/api/tools/confluence/user/route.ts | 83 ++++++++++++ apps/sim/blocks/blocks/confluence.ts | 37 ++++++ apps/sim/tools/confluence/get_user.ts | 118 ++++++++++++++++++ apps/sim/tools/confluence/index.ts | 3 + apps/sim/tools/registry.ts | 2 + 8 files changed, 274 insertions(+), 7 deletions(-) create mode 100644 apps/sim/app/api/tools/confluence/user/route.ts create mode 100644 apps/sim/tools/confluence/get_user.ts diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index 5121253240..7f36adb31a 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -38,8 +38,8 @@ import { EyeIcon, FirecrawlIcon, FirefliesIcon, - GithubIcon, GitLabIcon, + GithubIcon, GmailIcon, GongIcon, GoogleBooksIcon, @@ -72,9 +72,9 @@ import { LinearIcon, LinkedInIcon, LinkupIcon, + MailServerIcon, MailchimpIcon, MailgunIcon, - MailServerIcon, Mem0Icon, MicrosoftDataverseIcon, MicrosoftExcelIcon, @@ -107,6 +107,8 @@ import { ResendIcon, RevenueCatIcon, S3Icon, + SQSIcon, + STTIcon, SalesforceIcon, SearchIcon, SendgridIcon, @@ -118,19 +120,17 @@ import { SimilarwebIcon, SlackIcon, SmtpIcon, - SQSIcon, SshIcon, - STTIcon, StagehandIcon, StripeIcon, SupabaseIcon, + TTSIcon, TavilyIcon, TelegramIcon, TextractIcon, TinybirdIcon, TranslateIcon, TrelloIcon, - TTSIcon, TwilioIcon, TypeformIcon, UpstashIcon, @@ -141,11 +141,11 @@ import { WhatsAppIcon, WikipediaIcon, WordpressIcon, - xIcon, YouTubeIcon, ZendeskIcon, ZepIcon, ZoomIcon, + xIcon, } from '@/components/icons' type IconComponent = ComponentType> diff --git a/apps/docs/content/docs/en/tools/confluence.mdx b/apps/docs/content/docs/en/tools/confluence.mdx index 7ee0f0e73e..158f3d40ae 100644 --- a/apps/docs/content/docs/en/tools/confluence.mdx +++ b/apps/docs/content/docs/en/tools/confluence.mdx @@ -1040,4 +1040,28 @@ List all Confluence spaces accessible to the user. | ↳ `representation` | string | Content representation format \(e.g., plain, view, storage\) | | `nextCursor` | string | Cursor for fetching the next page of results | +### `confluence_get_user` + +Get a Confluence user\ + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `accountId` | string | Yes | The Atlassian account ID of the user to look up | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `accountId` | string | Atlassian account ID of the user | +| `displayName` | string | Display name of the user | +| `email` | string | Email address of the user | +| `accountType` | string | Account type \(e.g., atlassian, app, customer\) | +| `profilePicture` | string | Path to the user profile picture | +| `publicName` | string | Public name of the user | + diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index 9fc1cc577e..ce8f8e6b28 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -146,4 +146,4 @@ "zep", "zoom" ] -} +} \ No newline at end of file diff --git a/apps/sim/app/api/tools/confluence/user/route.ts b/apps/sim/app/api/tools/confluence/user/route.ts new file mode 100644 index 0000000000..d8523c0724 --- /dev/null +++ b/apps/sim/app/api/tools/confluence/user/route.ts @@ -0,0 +1,83 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' +import { getConfluenceCloudId } from '@/tools/confluence/utils' + +const logger = createLogger('ConfluenceUserAPI') + +export const dynamic = 'force-dynamic' + +/** + * Get a Confluence user by account ID. + * Uses GET /wiki/rest/api/user?accountId={accountId} + */ +export async function GET(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const { searchParams } = new URL(request.url) + const domain = searchParams.get('domain') + const accessToken = searchParams.get('accessToken') + const accountId = searchParams.get('accountId') + const providedCloudId = searchParams.get('cloudId') + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!accountId) { + return NextResponse.json({ error: 'Account ID is required' }, { status: 400 }) + } + + const accountIdValidation = validateAlphanumericId(accountId, 'accountId', 255) + if (!accountIdValidation.isValid) { + return NextResponse.json({ error: accountIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/rest/api/user?accountId=${encodeURIComponent(accountId)}` + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to get Confluence user (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + return NextResponse.json(data) + } catch (error) { + logger.error('Error getting Confluence user:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/blocks/blocks/confluence.ts b/apps/sim/blocks/blocks/confluence.ts index c17f44b89b..09343146c2 100644 --- a/apps/sim/blocks/blocks/confluence.ts +++ b/apps/sim/blocks/blocks/confluence.ts @@ -84,6 +84,7 @@ export const ConfluenceBlock: BlockConfig = { 'write:content.property:confluence', 'read:hierarchical-content:confluence', 'read:content.metadata:confluence', + 'read:user:confluence', ], placeholder: 'Select Confluence account', required: true, @@ -433,6 +434,8 @@ export const ConfluenceV2Block: BlockConfig = { // Space Operations { label: 'Get Space', id: 'get_space' }, { label: 'List Spaces', id: 'list_spaces' }, + // User Operations + { label: 'Get User', id: 'get_user' }, ], value: () => 'read', }, @@ -472,6 +475,7 @@ export const ConfluenceV2Block: BlockConfig = { 'write:content.property:confluence', 'read:hierarchical-content:confluence', 'read:content.metadata:confluence', + 'read:user:confluence', ], placeholder: 'Select Confluence account', required: true, @@ -514,6 +518,7 @@ export const ConfluenceV2Block: BlockConfig = { 'list_spaces', 'get_pages_by_label', 'list_space_labels', + 'get_user', ], not: true, }, @@ -560,6 +565,7 @@ export const ConfluenceV2Block: BlockConfig = { 'list_spaces', 'get_pages_by_label', 'list_space_labels', + 'get_user', ], not: true, }, @@ -621,6 +627,14 @@ export const ConfluenceV2Block: BlockConfig = { required: true, condition: { field: 'operation', value: 'get_page_version' }, }, + { + id: 'accountId', + title: 'Account ID', + type: 'short-input', + placeholder: 'Enter Atlassian account ID', + required: true, + condition: { field: 'operation', value: 'get_user' }, + }, { id: 'propertyKey', title: 'Property Key', @@ -922,6 +936,8 @@ export const ConfluenceV2Block: BlockConfig = { // Space Tools 'confluence_get_space', 'confluence_list_spaces', + // User Tools + 'confluence_get_user', ], config: { tool: (params) => { @@ -999,6 +1015,9 @@ export const ConfluenceV2Block: BlockConfig = { return 'confluence_get_space' case 'list_spaces': return 'confluence_list_spaces' + // User Operations + case 'get_user': + return 'confluence_get_user' default: return 'confluence_retrieve' } @@ -1013,6 +1032,7 @@ export const ConfluenceV2Block: BlockConfig = { attachmentComment, blogPostId, versionNumber, + accountId, propertyKey, propertyValue, propertyId, @@ -1152,6 +1172,15 @@ export const ConfluenceV2Block: BlockConfig = { } } + if (operation === 'get_user') { + return { + credential: oauthCredential, + operation, + accountId: accountId ? String(accountId).trim() : undefined, + ...rest, + } + } + return { credential: oauthCredential, pageId: effectivePageId || undefined, @@ -1171,6 +1200,7 @@ export const ConfluenceV2Block: BlockConfig = { spaceId: { type: 'string', description: 'Space identifier' }, blogPostId: { type: 'string', description: 'Blog post identifier' }, versionNumber: { type: 'number', description: 'Page version number' }, + accountId: { type: 'string', description: 'Atlassian account ID' }, propertyKey: { type: 'string', description: 'Property key/name' }, propertyValue: { type: 'json', description: 'Property value (JSON)' }, title: { type: 'string', description: 'Page or blog post title' }, @@ -1242,6 +1272,13 @@ export const ConfluenceV2Block: BlockConfig = { propertyId: { type: 'string', description: 'Property identifier' }, propertyKey: { type: 'string', description: 'Property key' }, propertyValue: { type: 'json', description: 'Property value' }, + // User Results + accountId: { type: 'string', description: 'Atlassian account ID' }, + displayName: { type: 'string', description: 'User display name' }, + email: { type: 'string', description: 'User email address' }, + accountType: { type: 'string', description: 'Account type (atlassian, app, customer)' }, + profilePicture: { type: 'string', description: 'Path to user profile picture' }, + publicName: { type: 'string', description: 'User public name' }, // Pagination nextCursor: { type: 'string', description: 'Cursor for fetching next page of results' }, }, diff --git a/apps/sim/tools/confluence/get_user.ts b/apps/sim/tools/confluence/get_user.ts new file mode 100644 index 0000000000..6935ac9a2d --- /dev/null +++ b/apps/sim/tools/confluence/get_user.ts @@ -0,0 +1,118 @@ +import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceGetUserParams { + accessToken: string + domain: string + accountId: string + cloudId?: string +} + +export interface ConfluenceGetUserResponse { + success: boolean + output: { + ts: string + accountId: string + displayName: string + email: string | null + accountType: string | null + profilePicture: string | null + publicName: string | null + } +} + +export const confluenceGetUserTool: ToolConfig< + ConfluenceGetUserParams, + ConfluenceGetUserResponse +> = { + id: 'confluence_get_user', + name: 'Confluence Get User', + description: 'Get a Confluence user\'s display name and profile info by their account ID.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + accountId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The Atlassian account ID of the user to look up', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: (params: ConfluenceGetUserParams) => { + const query = new URLSearchParams({ + domain: params.domain, + accessToken: params.accessToken, + accountId: params.accountId?.trim(), + }) + if (params.cloudId) { + query.set('cloudId', params.cloudId) + } + return `/api/tools/confluence/user?${query.toString()}` + }, + method: 'GET', + headers: (params: ConfluenceGetUserParams) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + accountId: data.accountId ?? '', + displayName: data.displayName ?? '', + email: data.email ?? null, + accountType: data.accountType ?? null, + profilePicture: data.profilePicture?.path ?? null, + publicName: data.publicName ?? null, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + accountId: { type: 'string', description: 'Atlassian account ID of the user' }, + displayName: { type: 'string', description: 'Display name of the user' }, + email: { type: 'string', description: 'Email address of the user', optional: true }, + accountType: { + type: 'string', + description: 'Account type (e.g., atlassian, app, customer)', + optional: true, + }, + profilePicture: { + type: 'string', + description: 'Path to the user profile picture', + optional: true, + }, + publicName: { type: 'string', description: 'Public name of the user', optional: true }, + }, +} diff --git a/apps/sim/tools/confluence/index.ts b/apps/sim/tools/confluence/index.ts index 2494f32d04..4dd64d9fdc 100644 --- a/apps/sim/tools/confluence/index.ts +++ b/apps/sim/tools/confluence/index.ts @@ -13,6 +13,7 @@ import { confluenceGetPageAncestorsTool } from '@/tools/confluence/get_page_ance import { confluenceGetPageChildrenTool } from '@/tools/confluence/get_page_children' import { confluenceGetPageVersionTool } from '@/tools/confluence/get_page_version' import { confluenceGetPagesByLabelTool } from '@/tools/confluence/get_pages_by_label' +import { confluenceGetUserTool } from '@/tools/confluence/get_user' import { confluenceGetSpaceTool } from '@/tools/confluence/get_space' import { confluenceListAttachmentsTool } from '@/tools/confluence/list_attachments' import { confluenceListBlogPostsTool } from '@/tools/confluence/list_blogposts' @@ -106,6 +107,8 @@ export { confluenceDeleteLabelTool, confluenceGetPagesByLabelTool, confluenceListSpaceLabelsTool, + // User Tools + confluenceGetUserTool, // Space Tools confluenceGetSpaceTool, confluenceListSpacesTool, diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 5a2f5787c7..22083eb2d6 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -201,6 +201,7 @@ import { confluenceGetPagesByLabelTool, confluenceGetPageVersionTool, confluenceGetSpaceTool, + confluenceGetUserTool, confluenceListAttachmentsTool, confluenceListBlogPostsInSpaceTool, confluenceListBlogPostsTool, @@ -2998,6 +2999,7 @@ export const tools: Record = { confluence_delete_label: confluenceDeleteLabelTool, confluence_delete_page_property: confluenceDeletePagePropertyTool, confluence_get_space: confluenceGetSpaceTool, + confluence_get_user: confluenceGetUserTool, confluence_list_spaces: confluenceListSpacesTool, cursor_list_agents: cursorListAgentsTool, cursor_list_agents_v2: cursorListAgentsV2Tool, From bf0bd8f3b2d97b2233f336a6cd532c7ad906e398 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 25 Feb 2026 14:43:16 -0800 Subject: [PATCH 02/16] feat(confluence): add missing tools for tasks, blog posts, spaces, descendants, permissions, and properties Add 16 new Confluence operations: list/get/update tasks, update/delete blog posts, create/update/delete spaces, get page descendants, list space permissions, list/create/delete space properties. Includes API routes, tool definitions, block config wiring, OAuth scopes, and generated docs. Co-Authored-By: Claude Opus 4.6 --- .../docs/content/docs/en/tools/confluence.mdx | 357 ++++++++++++++++++ .../api/tools/confluence/blogposts/route.ts | 156 ++++++++ .../confluence/page-descendants/route.ts | 105 ++++++ .../confluence/space-permissions/route.ts | 106 ++++++ .../confluence/space-properties/route.ts | 184 +++++++++ .../app/api/tools/confluence/space/route.ts | 232 ++++++++++++ .../app/api/tools/confluence/tasks/route.ts | 241 ++++++++++++ .../components/oauth-required-modal.tsx | 9 + apps/sim/blocks/blocks/confluence.ts | 331 +++++++++++++++- apps/sim/lib/oauth/oauth.ts | 15 + apps/sim/tools/confluence/create_space.ts | 134 +++++++ .../tools/confluence/create_space_property.ts | 118 ++++++ apps/sim/tools/confluence/delete_blogpost.ts | 95 +++++ apps/sim/tools/confluence/delete_space.ts | 95 +++++ .../tools/confluence/delete_space_property.ts | 107 ++++++ .../tools/confluence/get_page_descendants.ts | 139 +++++++ apps/sim/tools/confluence/get_task.ts | 129 +++++++ apps/sim/tools/confluence/index.ts | 31 +- .../confluence/list_space_permissions.ts | 141 +++++++ .../tools/confluence/list_space_properties.ts | 133 +++++++ apps/sim/tools/confluence/list_tasks.ts | 174 +++++++++ apps/sim/tools/confluence/update_blogpost.ts | 123 ++++++ apps/sim/tools/confluence/update_space.ts | 131 +++++++ apps/sim/tools/confluence/update_task.ts | 138 +++++++ apps/sim/tools/registry.ts | 26 ++ 25 files changed, 3446 insertions(+), 4 deletions(-) create mode 100644 apps/sim/app/api/tools/confluence/page-descendants/route.ts create mode 100644 apps/sim/app/api/tools/confluence/space-permissions/route.ts create mode 100644 apps/sim/app/api/tools/confluence/space-properties/route.ts create mode 100644 apps/sim/app/api/tools/confluence/tasks/route.ts create mode 100644 apps/sim/tools/confluence/create_space.ts create mode 100644 apps/sim/tools/confluence/create_space_property.ts create mode 100644 apps/sim/tools/confluence/delete_blogpost.ts create mode 100644 apps/sim/tools/confluence/delete_space.ts create mode 100644 apps/sim/tools/confluence/delete_space_property.ts create mode 100644 apps/sim/tools/confluence/get_page_descendants.ts create mode 100644 apps/sim/tools/confluence/get_task.ts create mode 100644 apps/sim/tools/confluence/list_space_permissions.ts create mode 100644 apps/sim/tools/confluence/list_space_properties.ts create mode 100644 apps/sim/tools/confluence/list_tasks.ts create mode 100644 apps/sim/tools/confluence/update_blogpost.ts create mode 100644 apps/sim/tools/confluence/update_space.ts create mode 100644 apps/sim/tools/confluence/update_task.ts diff --git a/apps/docs/content/docs/en/tools/confluence.mdx b/apps/docs/content/docs/en/tools/confluence.mdx index 158f3d40ae..598e1e9d38 100644 --- a/apps/docs/content/docs/en/tools/confluence.mdx +++ b/apps/docs/content/docs/en/tools/confluence.mdx @@ -1008,6 +1008,85 @@ Get details about a specific Confluence space. | ↳ `value` | string | Description text content | | ↳ `representation` | string | Content representation format \(e.g., plain, view, storage\) | +### `confluence_create_space` + +Create a new Confluence space. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `name` | string | Yes | Name for the new space | +| `key` | string | Yes | Unique key for the space \(uppercase, no spaces\) | +| `description` | string | No | Description for the new space | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `spaceId` | string | Created space ID | +| `name` | string | Space name | +| `key` | string | Space key | +| `type` | string | Space type | +| `status` | string | Space status | +| `url` | string | URL to view the space | +| `homepageId` | string | Homepage ID | +| `description` | object | Space description | +| ↳ `value` | string | Description text content | +| ↳ `representation` | string | Content representation format \(e.g., plain, view, storage\) | + +### `confluence_update_space` + +Update a Confluence space name or description. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `spaceId` | string | Yes | ID of the space to update | +| `name` | string | No | New name for the space | +| `description` | string | No | New description for the space | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `spaceId` | string | Updated space ID | +| `name` | string | Space name | +| `key` | string | Space key | +| `type` | string | Space type | +| `status` | string | Space status | +| `url` | string | URL to view the space | +| `description` | object | Space description | +| ↳ `value` | string | Description text content | +| ↳ `representation` | string | Content representation format \(e.g., plain, view, storage\) | + +### `confluence_delete_space` + +Delete a Confluence space. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `spaceId` | string | Yes | ID of the space to delete | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `spaceId` | string | Deleted space ID | +| `deleted` | boolean | Deletion status | + ### `confluence_list_spaces` List all Confluence spaces accessible to the user. @@ -1040,6 +1119,284 @@ List all Confluence spaces accessible to the user. | ↳ `representation` | string | Content representation format \(e.g., plain, view, storage\) | | `nextCursor` | string | Cursor for fetching the next page of results | +### `confluence_list_space_properties` + +List properties on a Confluence space. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `spaceId` | string | Yes | Space ID to list properties for | +| `limit` | number | No | Maximum number of properties to return \(default: 50, max: 250\) | +| `cursor` | string | No | Pagination cursor from previous response | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `properties` | array | Array of space properties | +| ↳ `id` | string | Property ID | +| ↳ `key` | string | Property key | +| ↳ `value` | json | Property value | +| `spaceId` | string | Space ID | +| `nextCursor` | string | Cursor for fetching the next page of results | + +### `confluence_create_space_property` + +Create a property on a Confluence space. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `spaceId` | string | Yes | Space ID to create the property on | +| `key` | string | Yes | Property key/name | +| `value` | json | No | Property value \(JSON\) | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `propertyId` | string | Created property ID | +| `key` | string | Property key | +| `value` | json | Property value | +| `spaceId` | string | Space ID | + +### `confluence_delete_space_property` + +Delete a property from a Confluence space. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `spaceId` | string | Yes | Space ID the property belongs to | +| `propertyId` | string | Yes | Property ID to delete | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `spaceId` | string | Space ID | +| `propertyId` | string | Deleted property ID | +| `deleted` | boolean | Deletion status | + +### `confluence_list_space_permissions` + +List permissions for a Confluence space. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `spaceId` | string | Yes | Space ID to list permissions for | +| `limit` | number | No | Maximum number of permissions to return \(default: 50, max: 250\) | +| `cursor` | string | No | Pagination cursor from previous response | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `permissions` | array | Array of space permissions | +| ↳ `id` | string | Permission ID | +| ↳ `principalType` | string | Principal type \(user, group, role\) | +| ↳ `principalId` | string | Principal ID | +| ↳ `operationKey` | string | Operation key \(read, create, delete, etc.\) | +| ↳ `operationTargetType` | string | Target type \(page, blogpost, space, etc.\) | +| ↳ `anonymousAccess` | boolean | Whether anonymous access is allowed | +| ↳ `unlicensedAccess` | boolean | Whether unlicensed access is allowed | +| `spaceId` | string | Space ID | +| `nextCursor` | string | Cursor for fetching the next page of results | + +### `confluence_get_page_descendants` + +Get all descendants of a Confluence page recursively. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `pageId` | string | Yes | Page ID to get descendants for | +| `limit` | number | No | Maximum number of descendants to return \(default: 50, max: 250\) | +| `cursor` | string | No | Pagination cursor from previous response | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `descendants` | array | Array of descendant pages | +| ↳ `id` | string | Page ID | +| ↳ `title` | string | Page title | +| ↳ `status` | string | Page status | +| ↳ `spaceId` | string | Space ID | +| ↳ `parentId` | string | Parent page ID | +| ↳ `childPosition` | number | Position among siblings | +| `pageId` | string | Parent page ID | +| `nextCursor` | string | Cursor for fetching the next page of results | + +### `confluence_list_tasks` + +List inline tasks from Confluence. Optionally filter by page, space, assignee, or status. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `pageId` | string | No | Filter tasks by page ID | +| `spaceId` | string | No | Filter tasks by space ID | +| `assignedTo` | string | No | Filter tasks by assignee account ID | +| `status` | string | No | Filter tasks by status \(complete or incomplete\) | +| `limit` | number | No | Maximum number of tasks to return \(default: 50, max: 250\) | +| `cursor` | string | No | Pagination cursor from previous response | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `tasks` | array | Array of Confluence tasks | +| ↳ `id` | string | Task ID | +| ↳ `localId` | string | Local task ID | +| ↳ `spaceId` | string | Space ID | +| ↳ `pageId` | string | Page ID | +| ↳ `blogPostId` | string | Blog post ID | +| ↳ `status` | string | Task status \(complete or incomplete\) | +| ↳ `createdBy` | string | Creator account ID | +| ↳ `assignedTo` | string | Assignee account ID | +| ↳ `completedBy` | string | Completer account ID | +| ↳ `createdAt` | string | Creation timestamp | +| ↳ `updatedAt` | string | Last update timestamp | +| ↳ `dueAt` | string | Due date | +| ↳ `completedAt` | string | Completion timestamp | +| `nextCursor` | string | Cursor for fetching the next page of results | + +### `confluence_get_task` + +Get a specific Confluence inline task by ID. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `taskId` | string | Yes | The ID of the task to retrieve | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `id` | string | Task ID | +| `localId` | string | Local task ID | +| `spaceId` | string | Space ID | +| `pageId` | string | Page ID | +| `blogPostId` | string | Blog post ID | +| `status` | string | Task status \(complete or incomplete\) | +| `createdBy` | string | Creator account ID | +| `assignedTo` | string | Assignee account ID | +| `completedBy` | string | Completer account ID | +| `createdAt` | string | Creation timestamp | +| `updatedAt` | string | Last update timestamp | +| `dueAt` | string | Due date | +| `completedAt` | string | Completion timestamp | + +### `confluence_update_task` + +Update the status of a Confluence inline task (complete or incomplete). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `taskId` | string | Yes | The ID of the task to update | +| `status` | string | Yes | New status for the task \(complete or incomplete\) | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `id` | string | Task ID | +| `localId` | string | Local task ID | +| `spaceId` | string | Space ID | +| `pageId` | string | Page ID | +| `blogPostId` | string | Blog post ID | +| `status` | string | Updated task status | +| `createdBy` | string | Creator account ID | +| `assignedTo` | string | Assignee account ID | +| `completedBy` | string | Completer account ID | +| `createdAt` | string | Creation timestamp | +| `updatedAt` | string | Last update timestamp | +| `dueAt` | string | Due date | +| `completedAt` | string | Completion timestamp | + +### `confluence_update_blogpost` + +Update an existing Confluence blog post title and/or content. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `blogPostId` | string | Yes | The ID of the blog post to update | +| `title` | string | No | New title for the blog post | +| `content` | string | No | New content for the blog post in storage format | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `blogPostId` | string | Updated blog post ID | +| `title` | string | Blog post title | +| `status` | string | Blog post status | +| `spaceId` | string | Space ID | +| `version` | json | Version information | +| `url` | string | URL to view the blog post | + +### `confluence_delete_blogpost` + +Delete a Confluence blog post. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) | +| `blogPostId` | string | Yes | The ID of the blog post to delete | +| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ts` | string | ISO 8601 timestamp of the operation | +| `blogPostId` | string | Deleted blog post ID | +| `deleted` | boolean | Deletion status | + ### `confluence_get_user` Get a Confluence user\ diff --git a/apps/sim/app/api/tools/confluence/blogposts/route.ts b/apps/sim/app/api/tools/confluence/blogposts/route.ts index c186d5ca58..27ccc9bac7 100644 --- a/apps/sim/app/api/tools/confluence/blogposts/route.ts +++ b/apps/sim/app/api/tools/confluence/blogposts/route.ts @@ -283,3 +283,159 @@ export async function POST(request: NextRequest) { ) } } + +/** + * Update a blog post + */ +export async function PUT(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const { domain, accessToken, blogPostId, title, content, cloudId: providedCloudId } = body + + if (!domain || !accessToken || !blogPostId) { + return NextResponse.json( + { error: 'Domain, access token, and blog post ID are required' }, + { status: 400 } + ) + } + + const blogPostIdValidation = validateAlphanumericId(blogPostId, 'blogPostId', 255) + if (!blogPostIdValidation.isValid) { + return NextResponse.json({ error: blogPostIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + // Fetch current blog post to get version number + const currentUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/blogposts/${blogPostId}` + const currentResponse = await fetch(currentUrl, { + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!currentResponse.ok) { + throw new Error(`Failed to fetch current blog post: ${currentResponse.status}`) + } + + const currentPost = await currentResponse.json() + const currentVersion = currentPost.version.number + + const updateBody: Record = { + id: blogPostId, + version: { number: currentVersion + 1 }, + status: 'current', + title: title || currentPost.title, + body: { + representation: 'storage', + value: content || currentPost.body?.storage?.value || '', + }, + } + + const response = await fetch(currentUrl, { + method: 'PUT', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify(updateBody), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to update blog post (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + return NextResponse.json(data) + } catch (error) { + logger.error('Error updating blog post:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} + +/** + * Delete a blog post + */ +export async function DELETE(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const { domain, accessToken, blogPostId, cloudId: providedCloudId } = body + + if (!domain || !accessToken || !blogPostId) { + return NextResponse.json( + { error: 'Domain, access token, and blog post ID are required' }, + { status: 400 } + ) + } + + const blogPostIdValidation = validateAlphanumericId(blogPostId, 'blogPostId', 255) + if (!blogPostIdValidation.isValid) { + return NextResponse.json({ error: blogPostIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/blogposts/${blogPostId}` + + const response = await fetch(url, { + method: 'DELETE', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to delete blog post (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + return NextResponse.json({ blogPostId, deleted: true }) + } catch (error) { + logger.error('Error deleting blog post:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/confluence/page-descendants/route.ts b/apps/sim/app/api/tools/confluence/page-descendants/route.ts new file mode 100644 index 0000000000..1789b36b58 --- /dev/null +++ b/apps/sim/app/api/tools/confluence/page-descendants/route.ts @@ -0,0 +1,105 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' +import { getConfluenceCloudId } from '@/tools/confluence/utils' + +const logger = createLogger('ConfluencePageDescendantsAPI') + +export const dynamic = 'force-dynamic' + +/** + * Get all descendants of a Confluence page recursively. + * Uses GET /wiki/api/v2/pages/{id}/descendants + */ +export async function POST(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const { domain, accessToken, pageId, cloudId: providedCloudId, limit = 50, cursor } = body + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!pageId) { + return NextResponse.json({ error: 'Page ID is required' }, { status: 400 }) + } + + const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255) + if (!pageIdValidation.isValid) { + return NextResponse.json({ error: pageIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const queryParams = new URLSearchParams() + queryParams.append('limit', String(Math.min(limit, 250))) + + if (cursor) { + queryParams.append('cursor', cursor) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/descendants?${queryParams.toString()}` + + logger.info(`Fetching descendants for page ${pageId}`) + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to get page descendants (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + + const descendants = (data.results || []).map((page: any) => ({ + id: page.id, + title: page.title, + status: page.status ?? null, + spaceId: page.spaceId ?? null, + parentId: page.parentId ?? null, + childPosition: page.childPosition ?? null, + })) + + return NextResponse.json({ + descendants, + pageId, + nextCursor: data._links?.next + ? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor') + : null, + }) + } catch (error) { + logger.error('Error getting page descendants:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/confluence/space-permissions/route.ts b/apps/sim/app/api/tools/confluence/space-permissions/route.ts new file mode 100644 index 0000000000..c1a063e2cf --- /dev/null +++ b/apps/sim/app/api/tools/confluence/space-permissions/route.ts @@ -0,0 +1,106 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' +import { getConfluenceCloudId } from '@/tools/confluence/utils' + +const logger = createLogger('ConfluenceSpacePermissionsAPI') + +export const dynamic = 'force-dynamic' + +/** + * List permissions for a Confluence space. + * Uses GET /wiki/api/v2/spaces/{id}/permissions + */ +export async function POST(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const { domain, accessToken, spaceId, cloudId: providedCloudId, limit = 50, cursor } = body + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!spaceId) { + return NextResponse.json({ error: 'Space ID is required' }, { status: 400 }) + } + + const spaceIdValidation = validateAlphanumericId(spaceId, 'spaceId', 255) + if (!spaceIdValidation.isValid) { + return NextResponse.json({ error: spaceIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const queryParams = new URLSearchParams() + queryParams.append('limit', String(Math.min(limit, 250))) + + if (cursor) { + queryParams.append('cursor', cursor) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}/permissions?${queryParams.toString()}` + + logger.info(`Fetching permissions for space ${spaceId}`) + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to list space permissions (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + + const permissions = (data.results || []).map((perm: any) => ({ + id: perm.id, + principalType: perm.principal?.type ?? null, + principalId: perm.principal?.id ?? null, + operationKey: perm.operation?.key ?? null, + operationTargetType: perm.operation?.targetType ?? null, + anonymousAccess: perm.anonymousAccess ?? false, + unlicensedAccess: perm.unlicensedAccess ?? false, + })) + + return NextResponse.json({ + permissions, + spaceId, + nextCursor: data._links?.next + ? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor') + : null, + }) + } catch (error) { + logger.error('Error listing space permissions:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/confluence/space-properties/route.ts b/apps/sim/app/api/tools/confluence/space-properties/route.ts new file mode 100644 index 0000000000..0bceb68c5b --- /dev/null +++ b/apps/sim/app/api/tools/confluence/space-properties/route.ts @@ -0,0 +1,184 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' +import { getConfluenceCloudId } from '@/tools/confluence/utils' + +const logger = createLogger('ConfluenceSpacePropertiesAPI') + +export const dynamic = 'force-dynamic' + +/** + * List, create, or delete space properties. + * Uses GET/POST /wiki/api/v2/spaces/{id}/properties + * and DELETE /wiki/api/v2/spaces/{id}/properties/{propertyId} + */ +export async function POST(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const { + domain, + accessToken, + spaceId, + cloudId: providedCloudId, + action, + key, + value, + propertyId, + limit = 50, + cursor, + } = body + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!spaceId) { + return NextResponse.json({ error: 'Space ID is required' }, { status: 400 }) + } + + const spaceIdValidation = validateAlphanumericId(spaceId, 'spaceId', 255) + if (!spaceIdValidation.isValid) { + return NextResponse.json({ error: spaceIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const baseUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}/properties` + + // Delete a property + if (action === 'delete' && propertyId) { + const propertyIdValidation = validateAlphanumericId(propertyId, 'propertyId', 255) + if (!propertyIdValidation.isValid) { + return NextResponse.json({ error: propertyIdValidation.error }, { status: 400 }) + } + + const url = `${baseUrl}/${propertyId}` + + logger.info(`Deleting space property ${propertyId} from space ${spaceId}`) + + const response = await fetch(url, { + method: 'DELETE', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to delete space property (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + return NextResponse.json({ spaceId, propertyId, deleted: true }) + } + + // Create a property + if (action === 'create' && key) { + logger.info(`Creating space property '${key}' on space ${spaceId}`) + + const response = await fetch(baseUrl, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ key, value: value ?? {} }), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to create space property (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + return NextResponse.json({ + propertyId: data.id, + key: data.key, + value: data.value ?? null, + spaceId, + }) + } + + // List properties + const queryParams = new URLSearchParams() + queryParams.append('limit', String(Math.min(limit, 250))) + + if (cursor) queryParams.append('cursor', cursor) + + const url = `${baseUrl}?${queryParams.toString()}` + + logger.info(`Fetching properties for space ${spaceId}`) + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to list space properties (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + + const properties = (data.results || []).map((prop: any) => ({ + id: prop.id, + key: prop.key, + value: prop.value ?? null, + })) + + return NextResponse.json({ + properties, + spaceId, + nextCursor: data._links?.next + ? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor') + : null, + }) + } catch (error) { + logger.error('Error with space properties:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/confluence/space/route.ts b/apps/sim/app/api/tools/confluence/space/route.ts index a8e0186f79..5ab71dbc1a 100644 --- a/apps/sim/app/api/tools/confluence/space/route.ts +++ b/apps/sim/app/api/tools/confluence/space/route.ts @@ -78,3 +78,235 @@ export async function GET(request: NextRequest) { ) } } + +/** + * Create a new Confluence space. + * Uses POST /wiki/api/v2/spaces + */ +export async function POST(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const { domain, accessToken, name, key, description, cloudId: providedCloudId } = body + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!name) { + return NextResponse.json({ error: 'Space name is required' }, { status: 400 }) + } + + if (!key) { + return NextResponse.json({ error: 'Space key is required' }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces` + + const createBody: Record = { name, key } + if (description) { + createBody.description = { value: description, representation: 'plain' } + } + + logger.info(`Creating space with key ${key}`) + + const response = await fetch(url, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify(createBody), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to create space (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + return NextResponse.json(data) + } catch (error) { + logger.error('Error creating Confluence space:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} + +/** + * Update a Confluence space. + * Uses PUT /wiki/api/v2/spaces/{id} + */ +export async function PUT(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const { domain, accessToken, spaceId, name, description, cloudId: providedCloudId } = body + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!spaceId) { + return NextResponse.json({ error: 'Space ID is required' }, { status: 400 }) + } + + const spaceIdValidation = validateAlphanumericId(spaceId, 'spaceId', 255) + if (!spaceIdValidation.isValid) { + return NextResponse.json({ error: spaceIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}` + + const updateBody: Record = {} + if (name) updateBody.name = name + if (description !== undefined) { + updateBody.description = { value: description, representation: 'plain' } + } + + logger.info(`Updating space ${spaceId}`) + + const response = await fetch(url, { + method: 'PUT', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify(updateBody), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to update space (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + return NextResponse.json(data) + } catch (error) { + logger.error('Error updating Confluence space:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} + +/** + * Delete a Confluence space. + * Uses DELETE /wiki/api/v2/spaces/{id} + */ +export async function DELETE(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const { domain, accessToken, spaceId, cloudId: providedCloudId } = body + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + if (!spaceId) { + return NextResponse.json({ error: 'Space ID is required' }, { status: 400 }) + } + + const spaceIdValidation = validateAlphanumericId(spaceId, 'spaceId', 255) + if (!spaceIdValidation.isValid) { + return NextResponse.json({ error: spaceIdValidation.error }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}` + + logger.info(`Deleting space ${spaceId}`) + + const response = await fetch(url, { + method: 'DELETE', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = + errorData?.message || `Failed to delete space (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + return NextResponse.json({ spaceId, deleted: true }) + } catch (error) { + logger.error('Error deleting Confluence space:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/confluence/tasks/route.ts b/apps/sim/app/api/tools/confluence/tasks/route.ts new file mode 100644 index 0000000000..9a8fb60510 --- /dev/null +++ b/apps/sim/app/api/tools/confluence/tasks/route.ts @@ -0,0 +1,241 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' +import { getConfluenceCloudId } from '@/tools/confluence/utils' + +const logger = createLogger('ConfluenceTasksAPI') + +export const dynamic = 'force-dynamic' + +/** + * List, get, or update Confluence inline tasks. + * Uses GET /wiki/api/v2/tasks, GET /wiki/api/v2/tasks/{id}, PUT /wiki/api/v2/tasks/{id} + */ +export async function POST(request: NextRequest) { + try { + const auth = await checkSessionOrInternalAuth(request) + if (!auth.success || !auth.userId) { + return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) + } + + const body = await request.json() + const { + domain, + accessToken, + cloudId: providedCloudId, + action, + taskId, + status: taskStatus, + pageId, + spaceId, + assignedTo, + limit = 50, + cursor, + } = body + + if (!domain) { + return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) + } + + if (!accessToken) { + return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) + } + + const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) + + const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') + if (!cloudIdValidation.isValid) { + return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) + } + + // Update a task + if (action === 'update' && taskId) { + const taskIdValidation = validateAlphanumericId(taskId, 'taskId', 255) + if (!taskIdValidation.isValid) { + return NextResponse.json({ error: taskIdValidation.error }, { status: 400 }) + } + + // First fetch the current task to get required fields + const getUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/tasks/${taskId}` + const getResponse = await fetch(getUrl, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!getResponse.ok) { + const errorData = await getResponse.json().catch(() => null) + const errorMessage = errorData?.message || `Failed to fetch task (${getResponse.status})` + return NextResponse.json({ error: errorMessage }, { status: getResponse.status }) + } + + const currentTask = await getResponse.json() + + const updateBody: Record = { + id: taskId, + status: taskStatus || currentTask.status, + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/tasks/${taskId}` + + logger.info(`Updating task ${taskId}`) + + const response = await fetch(url, { + method: 'PUT', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify(updateBody), + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = errorData?.message || `Failed to update task (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + return NextResponse.json({ + task: { + id: data.id, + localId: data.localId ?? null, + spaceId: data.spaceId ?? null, + pageId: data.pageId ?? null, + blogPostId: data.blogPostId ?? null, + status: data.status, + createdBy: data.createdBy ?? null, + assignedTo: data.assignedTo ?? null, + completedBy: data.completedBy ?? null, + createdAt: data.createdAt ?? null, + updatedAt: data.updatedAt ?? null, + dueAt: data.dueAt ?? null, + completedAt: data.completedAt ?? null, + }, + }) + } + + // Get a specific task + if (taskId) { + const taskIdValidation = validateAlphanumericId(taskId, 'taskId', 255) + if (!taskIdValidation.isValid) { + return NextResponse.json({ error: taskIdValidation.error }, { status: 400 }) + } + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/tasks/${taskId}` + + logger.info(`Fetching task ${taskId}`) + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = errorData?.message || `Failed to get task (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + return NextResponse.json({ + task: { + id: data.id, + localId: data.localId ?? null, + spaceId: data.spaceId ?? null, + pageId: data.pageId ?? null, + blogPostId: data.blogPostId ?? null, + status: data.status, + createdBy: data.createdBy ?? null, + assignedTo: data.assignedTo ?? null, + completedBy: data.completedBy ?? null, + createdAt: data.createdAt ?? null, + updatedAt: data.updatedAt ?? null, + dueAt: data.dueAt ?? null, + completedAt: data.completedAt ?? null, + }, + }) + } + + // List tasks + const queryParams = new URLSearchParams() + queryParams.append('limit', String(Math.min(limit, 250))) + + if (cursor) queryParams.append('cursor', cursor) + if (taskStatus) queryParams.append('status', taskStatus) + if (pageId) queryParams.append('page-id', pageId) + if (spaceId) queryParams.append('space-id', spaceId) + if (assignedTo) queryParams.append('assigned-to', assignedTo) + + const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/tasks?${queryParams.toString()}` + + logger.info('Fetching tasks') + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => null) + logger.error('Confluence API error response:', { + status: response.status, + statusText: response.statusText, + error: JSON.stringify(errorData, null, 2), + }) + const errorMessage = errorData?.message || `Failed to list tasks (${response.status})` + return NextResponse.json({ error: errorMessage }, { status: response.status }) + } + + const data = await response.json() + + const tasks = (data.results || []).map((task: any) => ({ + id: task.id, + localId: task.localId ?? null, + spaceId: task.spaceId ?? null, + pageId: task.pageId ?? null, + blogPostId: task.blogPostId ?? null, + status: task.status, + createdBy: task.createdBy ?? null, + assignedTo: task.assignedTo ?? null, + completedBy: task.completedBy ?? null, + createdAt: task.createdAt ?? null, + updatedAt: task.updatedAt ?? null, + dueAt: task.dueAt ?? null, + completedAt: task.completedAt ?? null, + })) + + return NextResponse.json({ + tasks, + nextCursor: data._links?.next + ? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor') + : null, + }) + } catch (error) { + logger.error('Error with tasks:', error) + return NextResponse.json( + { error: (error as Error).message || 'Internal server error' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx index 6cac32e626..9cb3abbf62 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx @@ -81,6 +81,15 @@ const SCOPE_DESCRIPTIONS: Record = { 'write:content.property:confluence': 'Create and manage content properties', 'read:hierarchical-content:confluence': 'View page hierarchy (children and ancestors)', 'read:content.metadata:confluence': 'View content metadata (required for ancestors)', + 'read:user:confluence': 'View Confluence user profiles', + 'read:task:confluence': 'View Confluence inline tasks', + 'write:task:confluence': 'Update Confluence inline tasks', + 'delete:blogpost:confluence': 'Delete Confluence blog posts', + 'write:space:confluence': 'Create and update Confluence spaces', + 'delete:space:confluence': 'Delete Confluence spaces', + 'read:space.property:confluence': 'View Confluence space properties', + 'write:space.property:confluence': 'Create and manage space properties', + 'read:space.permission:confluence': 'View Confluence space permissions', 'read:me': 'Read profile information', 'database.read': 'Read database', 'database.write': 'Write to database', diff --git a/apps/sim/blocks/blocks/confluence.ts b/apps/sim/blocks/blocks/confluence.ts index 09343146c2..1ef03831f3 100644 --- a/apps/sim/blocks/blocks/confluence.ts +++ b/apps/sim/blocks/blocks/confluence.ts @@ -415,6 +415,8 @@ export const ConfluenceV2Block: BlockConfig = { { label: 'List Blog Posts', id: 'list_blogposts' }, { label: 'Get Blog Post', id: 'get_blogpost' }, { label: 'Create Blog Post', id: 'create_blogpost' }, + { label: 'Update Blog Post', id: 'update_blogpost' }, + { label: 'Delete Blog Post', id: 'delete_blogpost' }, { label: 'List Blog Posts in Space', id: 'list_blogposts_in_space' }, // Comment Operations { label: 'Create Comment', id: 'create_comment' }, @@ -433,7 +435,22 @@ export const ConfluenceV2Block: BlockConfig = { { label: 'List Space Labels', id: 'list_space_labels' }, // Space Operations { label: 'Get Space', id: 'get_space' }, + { label: 'Create Space', id: 'create_space' }, + { label: 'Update Space', id: 'update_space' }, + { label: 'Delete Space', id: 'delete_space' }, { label: 'List Spaces', id: 'list_spaces' }, + // Space Property Operations + { label: 'List Space Properties', id: 'list_space_properties' }, + { label: 'Create Space Property', id: 'create_space_property' }, + { label: 'Delete Space Property', id: 'delete_space_property' }, + // Space Permission Operations + { label: 'List Space Permissions', id: 'list_space_permissions' }, + // Page Descendant Operations + { label: 'Get Page Descendants', id: 'get_page_descendants' }, + // Task Operations + { label: 'List Tasks', id: 'list_tasks' }, + { label: 'Get Task', id: 'get_task' }, + { label: 'Update Task', id: 'update_task' }, // User Operations { label: 'Get User', id: 'get_user' }, ], @@ -476,6 +493,14 @@ export const ConfluenceV2Block: BlockConfig = { 'read:hierarchical-content:confluence', 'read:content.metadata:confluence', 'read:user:confluence', + 'read:task:confluence', + 'write:task:confluence', + 'delete:blogpost:confluence', + 'write:space:confluence', + 'delete:space:confluence', + 'read:space.property:confluence', + 'write:space.property:confluence', + 'read:space.permission:confluence', ], placeholder: 'Select Confluence account', required: true, @@ -511,13 +536,25 @@ export const ConfluenceV2Block: BlockConfig = { 'list_pages_in_space', 'list_blogposts', 'get_blogpost', + 'update_blogpost', + 'delete_blogpost', 'list_blogposts_in_space', 'search', 'search_in_space', 'get_space', + 'create_space', + 'update_space', + 'delete_space', 'list_spaces', 'get_pages_by_label', 'list_space_labels', + 'list_space_permissions', + 'list_space_properties', + 'create_space_property', + 'delete_space_property', + 'list_tasks', + 'get_task', + 'update_task', 'get_user', ], not: true, @@ -542,6 +579,7 @@ export const ConfluenceV2Block: BlockConfig = { 'get_page_version', 'list_page_properties', 'create_page_property', + 'get_page_descendants', ], }, }, @@ -558,13 +596,25 @@ export const ConfluenceV2Block: BlockConfig = { 'list_pages_in_space', 'list_blogposts', 'get_blogpost', + 'update_blogpost', + 'delete_blogpost', 'list_blogposts_in_space', 'search', 'search_in_space', 'get_space', + 'create_space', + 'update_space', + 'delete_space', 'list_spaces', 'get_pages_by_label', 'list_space_labels', + 'list_space_permissions', + 'list_space_properties', + 'create_space_property', + 'delete_space_property', + 'list_tasks', + 'get_task', + 'update_task', 'get_user', ], not: true, @@ -589,6 +639,7 @@ export const ConfluenceV2Block: BlockConfig = { 'get_page_version', 'list_page_properties', 'create_page_property', + 'get_page_descendants', ], }, }, @@ -603,11 +654,18 @@ export const ConfluenceV2Block: BlockConfig = { value: [ 'create', 'get_space', + 'create_space', + 'update_space', + 'delete_space', 'list_pages_in_space', 'search_in_space', 'create_blogpost', 'list_blogposts_in_space', 'list_space_labels', + 'list_space_permissions', + 'list_space_properties', + 'create_space_property', + 'delete_space_property', ], }, }, @@ -617,7 +675,7 @@ export const ConfluenceV2Block: BlockConfig = { type: 'short-input', placeholder: 'Enter blog post ID', required: true, - condition: { field: 'operation', value: 'get_blogpost' }, + condition: { field: 'operation', value: ['get_blogpost', 'update_blogpost', 'delete_blogpost'] }, }, { id: 'versionNumber', @@ -635,6 +693,78 @@ export const ConfluenceV2Block: BlockConfig = { required: true, condition: { field: 'operation', value: 'get_user' }, }, + { + id: 'taskId', + title: 'Task ID', + type: 'short-input', + placeholder: 'Enter task ID', + required: true, + condition: { field: 'operation', value: ['get_task', 'update_task'] }, + }, + { + id: 'taskStatus', + title: 'Task Status', + type: 'dropdown', + options: [ + { label: 'Complete', id: 'complete' }, + { label: 'Incomplete', id: 'incomplete' }, + ], + value: () => 'complete', + condition: { field: 'operation', value: 'update_task' }, + }, + { + id: 'taskAssignedTo', + title: 'Assigned To', + type: 'short-input', + placeholder: 'Filter by assignee account ID (optional)', + condition: { field: 'operation', value: 'list_tasks' }, + }, + { + id: 'spaceName', + title: 'Space Name', + type: 'short-input', + placeholder: 'Enter space name', + required: true, + condition: { field: 'operation', value: 'create_space' }, + }, + { + id: 'spaceKey', + title: 'Space Key', + type: 'short-input', + placeholder: 'Enter space key (e.g., MYSPACE)', + required: true, + condition: { field: 'operation', value: 'create_space' }, + }, + { + id: 'spaceDescription', + title: 'Description', + type: 'long-input', + placeholder: 'Enter space description (optional)', + condition: { field: 'operation', value: ['create_space', 'update_space'] }, + }, + { + id: 'spacePropertyKey', + title: 'Property Key', + type: 'short-input', + placeholder: 'Enter property key/name', + required: true, + condition: { field: 'operation', value: 'create_space_property' }, + }, + { + id: 'spacePropertyValue', + title: 'Property Value', + type: 'long-input', + placeholder: 'Enter property value (JSON supported)', + condition: { field: 'operation', value: 'create_space_property' }, + }, + { + id: 'spacePropertyId', + title: 'Property ID', + type: 'short-input', + placeholder: 'Enter property ID to delete', + required: true, + condition: { field: 'operation', value: 'delete_space_property' }, + }, { id: 'propertyKey', title: 'Property Key', @@ -664,14 +794,14 @@ export const ConfluenceV2Block: BlockConfig = { title: 'Title', type: 'short-input', placeholder: 'Enter title', - condition: { field: 'operation', value: ['create', 'update', 'create_blogpost'] }, + condition: { field: 'operation', value: ['create', 'update', 'create_blogpost', 'update_blogpost', 'update_space'] }, }, { id: 'content', title: 'Content', type: 'long-input', placeholder: 'Enter content', - condition: { field: 'operation', value: ['create', 'update', 'create_blogpost'] }, + condition: { field: 'operation', value: ['create', 'update', 'create_blogpost', 'update_blogpost'] }, }, { id: 'parentId', @@ -827,6 +957,10 @@ export const ConfluenceV2Block: BlockConfig = { 'list_labels', 'get_pages_by_label', 'list_space_labels', + 'get_page_descendants', + 'list_space_permissions', + 'list_space_properties', + 'list_tasks', ], }, }, @@ -850,6 +984,10 @@ export const ConfluenceV2Block: BlockConfig = { 'list_labels', 'get_pages_by_label', 'list_space_labels', + 'get_page_descendants', + 'list_space_permissions', + 'list_space_properties', + 'list_tasks', ], }, }, @@ -935,7 +1073,25 @@ export const ConfluenceV2Block: BlockConfig = { 'confluence_list_space_labels', // Space Tools 'confluence_get_space', + 'confluence_create_space', + 'confluence_update_space', + 'confluence_delete_space', 'confluence_list_spaces', + // Space Property Tools + 'confluence_list_space_properties', + 'confluence_create_space_property', + 'confluence_delete_space_property', + // Space Permission Tools + 'confluence_list_space_permissions', + // Page Descendant Tools + 'confluence_get_page_descendants', + // Task Tools + 'confluence_list_tasks', + 'confluence_get_task', + 'confluence_update_task', + // Blog Post Update/Delete + 'confluence_update_blogpost', + 'confluence_delete_blogpost', // User Tools 'confluence_get_user', ], @@ -981,6 +1137,10 @@ export const ConfluenceV2Block: BlockConfig = { return 'confluence_get_blogpost' case 'create_blogpost': return 'confluence_create_blogpost' + case 'update_blogpost': + return 'confluence_update_blogpost' + case 'delete_blogpost': + return 'confluence_delete_blogpost' case 'list_blogposts_in_space': return 'confluence_list_blogposts_in_space' // Comment Operations @@ -1013,8 +1173,34 @@ export const ConfluenceV2Block: BlockConfig = { // Space Operations case 'get_space': return 'confluence_get_space' + case 'create_space': + return 'confluence_create_space' + case 'update_space': + return 'confluence_update_space' + case 'delete_space': + return 'confluence_delete_space' case 'list_spaces': return 'confluence_list_spaces' + // Space Property Operations + case 'list_space_properties': + return 'confluence_list_space_properties' + case 'create_space_property': + return 'confluence_create_space_property' + case 'delete_space_property': + return 'confluence_delete_space_property' + // Space Permission Operations + case 'list_space_permissions': + return 'confluence_list_space_permissions' + // Page Descendant Operations + case 'get_page_descendants': + return 'confluence_get_page_descendants' + // Task Operations + case 'list_tasks': + return 'confluence_list_tasks' + case 'get_task': + return 'confluence_get_task' + case 'update_task': + return 'confluence_update_task' // User Operations case 'get_user': return 'confluence_get_user' @@ -1042,6 +1228,15 @@ export const ConfluenceV2Block: BlockConfig = { purge, bodyFormat, cursor, + taskId, + taskStatus, + taskAssignedTo, + spaceName, + spaceKey, + spaceDescription, + spacePropertyKey, + spacePropertyValue, + spacePropertyId, ...rest } = params @@ -1101,6 +1296,10 @@ export const ConfluenceV2Block: BlockConfig = { 'list_page_versions', 'list_page_properties', 'list_labels', + 'get_page_descendants', + 'list_space_permissions', + 'list_space_properties', + 'list_tasks', ] if (supportsCursor.includes(operation) && cursor) { @@ -1181,6 +1380,113 @@ export const ConfluenceV2Block: BlockConfig = { } } + if (operation === 'update_blogpost' || operation === 'delete_blogpost') { + return { + credential: oauthCredential, + operation, + blogPostId: blogPostId || undefined, + ...rest, + } + } + + if (operation === 'create_space') { + return { + credential: oauthCredential, + operation, + name: spaceName, + key: spaceKey, + description: spaceDescription, + ...rest, + } + } + + if (operation === 'update_space') { + return { + credential: oauthCredential, + operation, + name: spaceName || rest.title, + description: spaceDescription, + ...rest, + } + } + + if (operation === 'delete_space') { + return { + credential: oauthCredential, + operation, + ...rest, + } + } + + if (operation === 'create_space_property') { + return { + credential: oauthCredential, + operation, + key: spacePropertyKey, + value: spacePropertyValue, + ...rest, + } + } + + if (operation === 'delete_space_property') { + return { + credential: oauthCredential, + operation, + propertyId: spacePropertyId, + ...rest, + } + } + + if (operation === 'list_space_permissions' || operation === 'list_space_properties') { + return { + credential: oauthCredential, + operation, + cursor: cursor || undefined, + ...rest, + } + } + + if (operation === 'get_page_descendants') { + return { + credential: oauthCredential, + pageId: effectivePageId, + operation, + cursor: cursor || undefined, + ...rest, + } + } + + if (operation === 'get_task') { + return { + credential: oauthCredential, + operation, + taskId, + ...rest, + } + } + + if (operation === 'update_task') { + return { + credential: oauthCredential, + operation, + taskId, + status: taskStatus, + ...rest, + } + } + + if (operation === 'list_tasks') { + return { + credential: oauthCredential, + operation, + pageId: effectivePageId || undefined, + assignedTo: taskAssignedTo || undefined, + status: taskStatus || undefined, + cursor: cursor || undefined, + ...rest, + } + } + return { credential: oauthCredential, pageId: effectivePageId || undefined, @@ -1222,6 +1528,15 @@ export const ConfluenceV2Block: BlockConfig = { bodyFormat: { type: 'string', description: 'Body format for comments' }, limit: { type: 'number', description: 'Maximum number of results' }, cursor: { type: 'string', description: 'Pagination cursor from previous response' }, + taskId: { type: 'string', description: 'Task identifier' }, + taskStatus: { type: 'string', description: 'Task status (complete or incomplete)' }, + taskAssignedTo: { type: 'string', description: 'Filter tasks by assignee account ID' }, + spaceName: { type: 'string', description: 'Space name for create/update' }, + spaceKey: { type: 'string', description: 'Space key for create' }, + spaceDescription: { type: 'string', description: 'Space description' }, + spacePropertyKey: { type: 'string', description: 'Space property key' }, + spacePropertyValue: { type: 'json', description: 'Space property value' }, + spacePropertyId: { type: 'string', description: 'Space property identifier' }, }, outputs: { ts: { type: 'string', description: 'Timestamp' }, @@ -1279,6 +1594,16 @@ export const ConfluenceV2Block: BlockConfig = { accountType: { type: 'string', description: 'Account type (atlassian, app, customer)' }, profilePicture: { type: 'string', description: 'Path to user profile picture' }, publicName: { type: 'string', description: 'User public name' }, + // Task Results + tasks: { type: 'array', description: 'List of tasks' }, + taskId: { type: 'string', description: 'Task identifier' }, + // Descendant Results + descendants: { type: 'array', description: 'List of descendant pages' }, + // Permission Results + permissions: { type: 'array', description: 'List of space permissions' }, + // Space Property Results + homepageId: { type: 'string', description: 'Space homepage ID' }, + description: { type: 'json', description: 'Space description' }, // Pagination nextCursor: { type: 'string', description: 'Cursor for fetching next page of results' }, }, diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index b890566334..1f169b3f4c 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -330,6 +330,21 @@ export const OAUTH_PROVIDERS: Record = { 'search:confluence', 'read:me', 'offline_access', + 'read:blogpost:confluence', + 'write:blogpost:confluence', + 'delete:blogpost:confluence', + 'read:content.property:confluence', + 'write:content.property:confluence', + 'read:hierarchical-content:confluence', + 'read:content.metadata:confluence', + 'read:user:confluence', + 'read:task:confluence', + 'write:task:confluence', + 'write:space:confluence', + 'delete:space:confluence', + 'read:space.property:confluence', + 'write:space.property:confluence', + 'read:space.permission:confluence', ], }, }, diff --git a/apps/sim/tools/confluence/create_space.ts b/apps/sim/tools/confluence/create_space.ts new file mode 100644 index 0000000000..f2d8b8a734 --- /dev/null +++ b/apps/sim/tools/confluence/create_space.ts @@ -0,0 +1,134 @@ +import { SPACE_DESCRIPTION_OUTPUT_PROPERTIES, TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceCreateSpaceParams { + accessToken: string + domain: string + name: string + key: string + description?: string + cloudId?: string +} + +export interface ConfluenceCreateSpaceResponse { + success: boolean + output: { + ts: string + spaceId: string + name: string + key: string + type: string + status: string + url: string + homepageId: string | null + description: { value: string; representation: string } | null + } +} + +export const confluenceCreateSpaceTool: ToolConfig< + ConfluenceCreateSpaceParams, + ConfluenceCreateSpaceResponse +> = { + id: 'confluence_create_space', + name: 'Confluence Create Space', + description: 'Create a new Confluence space.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name for the new space', + }, + key: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Unique key for the space (uppercase, no spaces)', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Description for the new space', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/space', + method: 'POST', + headers: (params: ConfluenceCreateSpaceParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceCreateSpaceParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + name: params.name, + key: params.key, + description: params.description, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + spaceId: data.id ?? '', + name: data.name ?? '', + key: data.key ?? '', + type: data.type ?? '', + status: data.status ?? '', + url: data._links?.webui ?? '', + homepageId: data.homepageId ?? null, + description: data.description ?? null, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + spaceId: { type: 'string', description: 'Created space ID' }, + name: { type: 'string', description: 'Space name' }, + key: { type: 'string', description: 'Space key' }, + type: { type: 'string', description: 'Space type' }, + status: { type: 'string', description: 'Space status' }, + url: { type: 'string', description: 'URL to view the space' }, + homepageId: { type: 'string', description: 'Homepage ID', optional: true }, + description: { + type: 'object', + description: 'Space description', + properties: SPACE_DESCRIPTION_OUTPUT_PROPERTIES, + optional: true, + }, + }, +} diff --git a/apps/sim/tools/confluence/create_space_property.ts b/apps/sim/tools/confluence/create_space_property.ts new file mode 100644 index 0000000000..c702f63539 --- /dev/null +++ b/apps/sim/tools/confluence/create_space_property.ts @@ -0,0 +1,118 @@ +import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceCreateSpacePropertyParams { + accessToken: string + domain: string + spaceId: string + key: string + value?: unknown + cloudId?: string +} + +export interface ConfluenceCreateSpacePropertyResponse { + success: boolean + output: { + ts: string + propertyId: string + key: string + value: unknown + spaceId: string + } +} + +export const confluenceCreateSpacePropertyTool: ToolConfig< + ConfluenceCreateSpacePropertyParams, + ConfluenceCreateSpacePropertyResponse +> = { + id: 'confluence_create_space_property', + name: 'Confluence Create Space Property', + description: 'Create a property on a Confluence space.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + spaceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Space ID to create the property on', + }, + key: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Property key/name', + }, + value: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Property value (JSON)', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/space-properties', + method: 'POST', + headers: (params: ConfluenceCreateSpacePropertyParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceCreateSpacePropertyParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + spaceId: params.spaceId, + action: 'create', + key: params.key, + value: params.value, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + propertyId: data.propertyId ?? '', + key: data.key ?? '', + value: data.value ?? null, + spaceId: data.spaceId ?? '', + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + propertyId: { type: 'string', description: 'Created property ID' }, + key: { type: 'string', description: 'Property key' }, + value: { type: 'json', description: 'Property value' }, + spaceId: { type: 'string', description: 'Space ID' }, + }, +} diff --git a/apps/sim/tools/confluence/delete_blogpost.ts b/apps/sim/tools/confluence/delete_blogpost.ts new file mode 100644 index 0000000000..c53562cf28 --- /dev/null +++ b/apps/sim/tools/confluence/delete_blogpost.ts @@ -0,0 +1,95 @@ +import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceDeleteBlogPostParams { + accessToken: string + domain: string + blogPostId: string + cloudId?: string +} + +export interface ConfluenceDeleteBlogPostResponse { + success: boolean + output: { + ts: string + blogPostId: string + deleted: boolean + } +} + +export const confluenceDeleteBlogPostTool: ToolConfig< + ConfluenceDeleteBlogPostParams, + ConfluenceDeleteBlogPostResponse +> = { + id: 'confluence_delete_blogpost', + name: 'Confluence Delete Blog Post', + description: 'Delete a Confluence blog post.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + blogPostId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the blog post to delete', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/blogposts', + method: 'DELETE', + headers: (params: ConfluenceDeleteBlogPostParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceDeleteBlogPostParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + blogPostId: params.blogPostId, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + blogPostId: data.blogPostId ?? '', + deleted: true, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + blogPostId: { type: 'string', description: 'Deleted blog post ID' }, + deleted: { type: 'boolean', description: 'Deletion status' }, + }, +} diff --git a/apps/sim/tools/confluence/delete_space.ts b/apps/sim/tools/confluence/delete_space.ts new file mode 100644 index 0000000000..82442c66ad --- /dev/null +++ b/apps/sim/tools/confluence/delete_space.ts @@ -0,0 +1,95 @@ +import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceDeleteSpaceParams { + accessToken: string + domain: string + spaceId: string + cloudId?: string +} + +export interface ConfluenceDeleteSpaceResponse { + success: boolean + output: { + ts: string + spaceId: string + deleted: boolean + } +} + +export const confluenceDeleteSpaceTool: ToolConfig< + ConfluenceDeleteSpaceParams, + ConfluenceDeleteSpaceResponse +> = { + id: 'confluence_delete_space', + name: 'Confluence Delete Space', + description: 'Delete a Confluence space.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + spaceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the space to delete', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/space', + method: 'DELETE', + headers: (params: ConfluenceDeleteSpaceParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceDeleteSpaceParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + spaceId: params.spaceId, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + spaceId: data.spaceId ?? '', + deleted: true, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + spaceId: { type: 'string', description: 'Deleted space ID' }, + deleted: { type: 'boolean', description: 'Deletion status' }, + }, +} diff --git a/apps/sim/tools/confluence/delete_space_property.ts b/apps/sim/tools/confluence/delete_space_property.ts new file mode 100644 index 0000000000..9c69431aac --- /dev/null +++ b/apps/sim/tools/confluence/delete_space_property.ts @@ -0,0 +1,107 @@ +import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceDeleteSpacePropertyParams { + accessToken: string + domain: string + spaceId: string + propertyId: string + cloudId?: string +} + +export interface ConfluenceDeleteSpacePropertyResponse { + success: boolean + output: { + ts: string + spaceId: string + propertyId: string + deleted: boolean + } +} + +export const confluenceDeleteSpacePropertyTool: ToolConfig< + ConfluenceDeleteSpacePropertyParams, + ConfluenceDeleteSpacePropertyResponse +> = { + id: 'confluence_delete_space_property', + name: 'Confluence Delete Space Property', + description: 'Delete a property from a Confluence space.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + spaceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Space ID the property belongs to', + }, + propertyId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Property ID to delete', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/space-properties', + method: 'POST', + headers: (params: ConfluenceDeleteSpacePropertyParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceDeleteSpacePropertyParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + spaceId: params.spaceId, + action: 'delete', + propertyId: params.propertyId, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + spaceId: data.spaceId ?? '', + propertyId: data.propertyId ?? '', + deleted: true, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + spaceId: { type: 'string', description: 'Space ID' }, + propertyId: { type: 'string', description: 'Deleted property ID' }, + deleted: { type: 'boolean', description: 'Deletion status' }, + }, +} diff --git a/apps/sim/tools/confluence/get_page_descendants.ts b/apps/sim/tools/confluence/get_page_descendants.ts new file mode 100644 index 0000000000..3caf48592b --- /dev/null +++ b/apps/sim/tools/confluence/get_page_descendants.ts @@ -0,0 +1,139 @@ +import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceGetPageDescendantsParams { + accessToken: string + domain: string + pageId: string + limit?: number + cursor?: string + cloudId?: string +} + +export interface ConfluenceGetPageDescendantsResponse { + success: boolean + output: { + ts: string + descendants: Array<{ + id: string + title: string + status: string | null + spaceId: string | null + parentId: string | null + childPosition: number | null + }> + pageId: string + nextCursor: string | null + } +} + +export const confluenceGetPageDescendantsTool: ToolConfig< + ConfluenceGetPageDescendantsParams, + ConfluenceGetPageDescendantsResponse +> = { + id: 'confluence_get_page_descendants', + name: 'Confluence Get Page Descendants', + description: 'Get all descendants of a Confluence page recursively.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + pageId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Page ID to get descendants for', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of descendants to return (default: 50, max: 250)', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from previous response', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/page-descendants', + method: 'POST', + headers: (params: ConfluenceGetPageDescendantsParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceGetPageDescendantsParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + pageId: params.pageId, + limit: params.limit, + cursor: params.cursor, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + descendants: data.descendants || [], + pageId: data.pageId ?? '', + nextCursor: data.nextCursor ?? null, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + descendants: { + type: 'array', + description: 'Array of descendant pages', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Page ID' }, + title: { type: 'string', description: 'Page title' }, + status: { type: 'string', description: 'Page status', optional: true }, + spaceId: { type: 'string', description: 'Space ID', optional: true }, + parentId: { type: 'string', description: 'Parent page ID', optional: true }, + childPosition: { type: 'number', description: 'Position among siblings', optional: true }, + }, + }, + }, + pageId: { type: 'string', description: 'Parent page ID' }, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/confluence/get_task.ts b/apps/sim/tools/confluence/get_task.ts new file mode 100644 index 0000000000..fddfaa9012 --- /dev/null +++ b/apps/sim/tools/confluence/get_task.ts @@ -0,0 +1,129 @@ +import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceGetTaskParams { + accessToken: string + domain: string + taskId: string + cloudId?: string +} + +export interface ConfluenceGetTaskResponse { + success: boolean + output: { + ts: string + id: string + localId: string | null + spaceId: string | null + pageId: string | null + blogPostId: string | null + status: string + createdBy: string | null + assignedTo: string | null + completedBy: string | null + createdAt: string | null + updatedAt: string | null + dueAt: string | null + completedAt: string | null + } +} + +export const confluenceGetTaskTool: ToolConfig< + ConfluenceGetTaskParams, + ConfluenceGetTaskResponse +> = { + id: 'confluence_get_task', + name: 'Confluence Get Task', + description: 'Get a specific Confluence inline task by ID.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + taskId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the task to retrieve', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/tasks', + method: 'POST', + headers: (params: ConfluenceGetTaskParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceGetTaskParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + taskId: params.taskId, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + const task = data.task || data + return { + success: true, + output: { + ts: new Date().toISOString(), + id: task.id ?? '', + localId: task.localId ?? null, + spaceId: task.spaceId ?? null, + pageId: task.pageId ?? null, + blogPostId: task.blogPostId ?? null, + status: task.status ?? '', + createdBy: task.createdBy ?? null, + assignedTo: task.assignedTo ?? null, + completedBy: task.completedBy ?? null, + createdAt: task.createdAt ?? null, + updatedAt: task.updatedAt ?? null, + dueAt: task.dueAt ?? null, + completedAt: task.completedAt ?? null, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + id: { type: 'string', description: 'Task ID' }, + localId: { type: 'string', description: 'Local task ID', optional: true }, + spaceId: { type: 'string', description: 'Space ID', optional: true }, + pageId: { type: 'string', description: 'Page ID', optional: true }, + blogPostId: { type: 'string', description: 'Blog post ID', optional: true }, + status: { type: 'string', description: 'Task status (complete or incomplete)' }, + createdBy: { type: 'string', description: 'Creator account ID', optional: true }, + assignedTo: { type: 'string', description: 'Assignee account ID', optional: true }, + completedBy: { type: 'string', description: 'Completer account ID', optional: true }, + createdAt: { type: 'string', description: 'Creation timestamp', optional: true }, + updatedAt: { type: 'string', description: 'Last update timestamp', optional: true }, + dueAt: { type: 'string', description: 'Due date', optional: true }, + completedAt: { type: 'string', description: 'Completion timestamp', optional: true }, + }, +} diff --git a/apps/sim/tools/confluence/index.ts b/apps/sim/tools/confluence/index.ts index 4dd64d9fdc..0dbcf4024c 100644 --- a/apps/sim/tools/confluence/index.ts +++ b/apps/sim/tools/confluence/index.ts @@ -3,18 +3,25 @@ import { confluenceCreateBlogPostTool } from '@/tools/confluence/create_blogpost import { confluenceCreateCommentTool } from '@/tools/confluence/create_comment' import { confluenceCreatePageTool } from '@/tools/confluence/create_page' import { confluenceCreatePagePropertyTool } from '@/tools/confluence/create_page_property' +import { confluenceCreateSpaceTool } from '@/tools/confluence/create_space' +import { confluenceCreateSpacePropertyTool } from '@/tools/confluence/create_space_property' import { confluenceDeleteAttachmentTool } from '@/tools/confluence/delete_attachment' +import { confluenceDeleteBlogPostTool } from '@/tools/confluence/delete_blogpost' import { confluenceDeleteCommentTool } from '@/tools/confluence/delete_comment' import { confluenceDeleteLabelTool } from '@/tools/confluence/delete_label' import { confluenceDeletePageTool } from '@/tools/confluence/delete_page' import { confluenceDeletePagePropertyTool } from '@/tools/confluence/delete_page_property' +import { confluenceDeleteSpaceTool } from '@/tools/confluence/delete_space' +import { confluenceDeleteSpacePropertyTool } from '@/tools/confluence/delete_space_property' import { confluenceGetBlogPostTool } from '@/tools/confluence/get_blogpost' import { confluenceGetPageAncestorsTool } from '@/tools/confluence/get_page_ancestors' import { confluenceGetPageChildrenTool } from '@/tools/confluence/get_page_children' +import { confluenceGetPageDescendantsTool } from '@/tools/confluence/get_page_descendants' import { confluenceGetPageVersionTool } from '@/tools/confluence/get_page_version' import { confluenceGetPagesByLabelTool } from '@/tools/confluence/get_pages_by_label' -import { confluenceGetUserTool } from '@/tools/confluence/get_user' import { confluenceGetSpaceTool } from '@/tools/confluence/get_space' +import { confluenceGetTaskTool } from '@/tools/confluence/get_task' +import { confluenceGetUserTool } from '@/tools/confluence/get_user' import { confluenceListAttachmentsTool } from '@/tools/confluence/list_attachments' import { confluenceListBlogPostsTool } from '@/tools/confluence/list_blogposts' import { confluenceListBlogPostsInSpaceTool } from '@/tools/confluence/list_blogposts_in_space' @@ -24,7 +31,10 @@ import { confluenceListPagePropertiesTool } from '@/tools/confluence/list_page_p import { confluenceListPageVersionsTool } from '@/tools/confluence/list_page_versions' import { confluenceListPagesInSpaceTool } from '@/tools/confluence/list_pages_in_space' import { confluenceListSpaceLabelsTool } from '@/tools/confluence/list_space_labels' +import { confluenceListSpacePermissionsTool } from '@/tools/confluence/list_space_permissions' +import { confluenceListSpacePropertiesTool } from '@/tools/confluence/list_space_properties' import { confluenceListSpacesTool } from '@/tools/confluence/list_spaces' +import { confluenceListTasksTool } from '@/tools/confluence/list_tasks' import { confluenceRetrieveTool } from '@/tools/confluence/retrieve' import { confluenceSearchTool } from '@/tools/confluence/search' import { confluenceSearchInSpaceTool } from '@/tools/confluence/search_in_space' @@ -65,7 +75,10 @@ import { VERSION_OUTPUT_PROPERTIES, } from '@/tools/confluence/types' import { confluenceUpdateTool } from '@/tools/confluence/update' +import { confluenceUpdateBlogPostTool } from '@/tools/confluence/update_blogpost' import { confluenceUpdateCommentTool } from '@/tools/confluence/update_comment' +import { confluenceUpdateSpaceTool } from '@/tools/confluence/update_space' +import { confluenceUpdateTaskTool } from '@/tools/confluence/update_task' import { confluenceUploadAttachmentTool } from '@/tools/confluence/upload_attachment' export { @@ -77,6 +90,7 @@ export { confluenceListPagesInSpaceTool, confluenceGetPageChildrenTool, confluenceGetPageAncestorsTool, + confluenceGetPageDescendantsTool, // Page Version Tools confluenceListPageVersionsTool, confluenceGetPageVersionTool, @@ -88,6 +102,8 @@ export { confluenceListBlogPostsTool, confluenceGetBlogPostTool, confluenceCreateBlogPostTool, + confluenceUpdateBlogPostTool, + confluenceDeleteBlogPostTool, confluenceListBlogPostsInSpaceTool, // Search Tools confluenceSearchTool, @@ -111,7 +127,20 @@ export { confluenceGetUserTool, // Space Tools confluenceGetSpaceTool, + confluenceCreateSpaceTool, + confluenceUpdateSpaceTool, + confluenceDeleteSpaceTool, confluenceListSpacesTool, + // Space Property Tools + confluenceListSpacePropertiesTool, + confluenceCreateSpacePropertyTool, + confluenceDeleteSpacePropertyTool, + // Space Permission Tools + confluenceListSpacePermissionsTool, + // Task Tools + confluenceListTasksTool, + confluenceGetTaskTool, + confluenceUpdateTaskTool, // Item property constants (for use in outputs) ATTACHMENT_ITEM_PROPERTIES, COMMENT_ITEM_PROPERTIES, diff --git a/apps/sim/tools/confluence/list_space_permissions.ts b/apps/sim/tools/confluence/list_space_permissions.ts new file mode 100644 index 0000000000..f47c559dbd --- /dev/null +++ b/apps/sim/tools/confluence/list_space_permissions.ts @@ -0,0 +1,141 @@ +import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceListSpacePermissionsParams { + accessToken: string + domain: string + spaceId: string + limit?: number + cursor?: string + cloudId?: string +} + +export interface ConfluenceListSpacePermissionsResponse { + success: boolean + output: { + ts: string + permissions: Array<{ + id: string + principalType: string | null + principalId: string | null + operationKey: string | null + operationTargetType: string | null + anonymousAccess: boolean + unlicensedAccess: boolean + }> + spaceId: string + nextCursor: string | null + } +} + +export const confluenceListSpacePermissionsTool: ToolConfig< + ConfluenceListSpacePermissionsParams, + ConfluenceListSpacePermissionsResponse +> = { + id: 'confluence_list_space_permissions', + name: 'Confluence List Space Permissions', + description: 'List permissions for a Confluence space.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + spaceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Space ID to list permissions for', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of permissions to return (default: 50, max: 250)', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from previous response', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/space-permissions', + method: 'POST', + headers: (params: ConfluenceListSpacePermissionsParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceListSpacePermissionsParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + spaceId: params.spaceId, + limit: params.limit, + cursor: params.cursor, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + permissions: data.permissions || [], + spaceId: data.spaceId ?? '', + nextCursor: data.nextCursor ?? null, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + permissions: { + type: 'array', + description: 'Array of space permissions', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Permission ID' }, + principalType: { type: 'string', description: 'Principal type (user, group, role)', optional: true }, + principalId: { type: 'string', description: 'Principal ID', optional: true }, + operationKey: { type: 'string', description: 'Operation key (read, create, delete, etc.)', optional: true }, + operationTargetType: { type: 'string', description: 'Target type (page, blogpost, space, etc.)', optional: true }, + anonymousAccess: { type: 'boolean', description: 'Whether anonymous access is allowed' }, + unlicensedAccess: { type: 'boolean', description: 'Whether unlicensed access is allowed' }, + }, + }, + }, + spaceId: { type: 'string', description: 'Space ID' }, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/confluence/list_space_properties.ts b/apps/sim/tools/confluence/list_space_properties.ts new file mode 100644 index 0000000000..d47c4570b0 --- /dev/null +++ b/apps/sim/tools/confluence/list_space_properties.ts @@ -0,0 +1,133 @@ +import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceListSpacePropertiesParams { + accessToken: string + domain: string + spaceId: string + limit?: number + cursor?: string + cloudId?: string +} + +export interface ConfluenceListSpacePropertiesResponse { + success: boolean + output: { + ts: string + properties: Array<{ + id: string + key: string + value: unknown + }> + spaceId: string + nextCursor: string | null + } +} + +export const confluenceListSpacePropertiesTool: ToolConfig< + ConfluenceListSpacePropertiesParams, + ConfluenceListSpacePropertiesResponse +> = { + id: 'confluence_list_space_properties', + name: 'Confluence List Space Properties', + description: 'List properties on a Confluence space.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + spaceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Space ID to list properties for', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of properties to return (default: 50, max: 250)', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from previous response', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/space-properties', + method: 'POST', + headers: (params: ConfluenceListSpacePropertiesParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceListSpacePropertiesParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + spaceId: params.spaceId, + limit: params.limit, + cursor: params.cursor, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + properties: data.properties || [], + spaceId: data.spaceId ?? '', + nextCursor: data.nextCursor ?? null, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + properties: { + type: 'array', + description: 'Array of space properties', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Property ID' }, + key: { type: 'string', description: 'Property key' }, + value: { type: 'json', description: 'Property value' }, + }, + }, + }, + spaceId: { type: 'string', description: 'Space ID' }, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/confluence/list_tasks.ts b/apps/sim/tools/confluence/list_tasks.ts new file mode 100644 index 0000000000..43edf00eb3 --- /dev/null +++ b/apps/sim/tools/confluence/list_tasks.ts @@ -0,0 +1,174 @@ +import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceListTasksParams { + accessToken: string + domain: string + pageId?: string + spaceId?: string + assignedTo?: string + status?: string + limit?: number + cursor?: string + cloudId?: string +} + +export interface ConfluenceListTasksResponse { + success: boolean + output: { + ts: string + tasks: Array<{ + id: string + localId: string | null + spaceId: string | null + pageId: string | null + blogPostId: string | null + status: string + createdBy: string | null + assignedTo: string | null + completedBy: string | null + createdAt: string | null + updatedAt: string | null + dueAt: string | null + completedAt: string | null + }> + nextCursor: string | null + } +} + +export const confluenceListTasksTool: ToolConfig< + ConfluenceListTasksParams, + ConfluenceListTasksResponse +> = { + id: 'confluence_list_tasks', + name: 'Confluence List Tasks', + description: 'List inline tasks from Confluence. Optionally filter by page, space, assignee, or status.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + pageId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter tasks by page ID', + }, + spaceId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter tasks by space ID', + }, + assignedTo: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter tasks by assignee account ID', + }, + status: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter tasks by status (complete or incomplete)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of tasks to return (default: 50, max: 250)', + }, + cursor: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Pagination cursor from previous response', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/tasks', + method: 'POST', + headers: (params: ConfluenceListTasksParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceListTasksParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + pageId: params.pageId, + spaceId: params.spaceId, + assignedTo: params.assignedTo, + status: params.status, + limit: params.limit, + cursor: params.cursor, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + tasks: data.tasks || [], + nextCursor: data.nextCursor ?? null, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + tasks: { + type: 'array', + description: 'Array of Confluence tasks', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Task ID' }, + localId: { type: 'string', description: 'Local task ID', optional: true }, + spaceId: { type: 'string', description: 'Space ID', optional: true }, + pageId: { type: 'string', description: 'Page ID', optional: true }, + blogPostId: { type: 'string', description: 'Blog post ID', optional: true }, + status: { type: 'string', description: 'Task status (complete or incomplete)' }, + createdBy: { type: 'string', description: 'Creator account ID', optional: true }, + assignedTo: { type: 'string', description: 'Assignee account ID', optional: true }, + completedBy: { type: 'string', description: 'Completer account ID', optional: true }, + createdAt: { type: 'string', description: 'Creation timestamp', optional: true }, + updatedAt: { type: 'string', description: 'Last update timestamp', optional: true }, + dueAt: { type: 'string', description: 'Due date', optional: true }, + completedAt: { type: 'string', description: 'Completion timestamp', optional: true }, + }, + }, + }, + nextCursor: { + type: 'string', + description: 'Cursor for fetching the next page of results', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/confluence/update_blogpost.ts b/apps/sim/tools/confluence/update_blogpost.ts new file mode 100644 index 0000000000..ea873cea17 --- /dev/null +++ b/apps/sim/tools/confluence/update_blogpost.ts @@ -0,0 +1,123 @@ +import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceUpdateBlogPostParams { + accessToken: string + domain: string + blogPostId: string + title?: string + content?: string + cloudId?: string +} + +export interface ConfluenceUpdateBlogPostResponse { + success: boolean + output: { + ts: string + blogPostId: string + title: string + status: string | null + spaceId: string | null + version: Record | null + url: string + } +} + +export const confluenceUpdateBlogPostTool: ToolConfig< + ConfluenceUpdateBlogPostParams, + ConfluenceUpdateBlogPostResponse +> = { + id: 'confluence_update_blogpost', + name: 'Confluence Update Blog Post', + description: 'Update an existing Confluence blog post title and/or content.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + blogPostId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the blog post to update', + }, + title: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New title for the blog post', + }, + content: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New content for the blog post in storage format', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/blogposts', + method: 'PUT', + headers: (params: ConfluenceUpdateBlogPostParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceUpdateBlogPostParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + blogPostId: params.blogPostId, + title: params.title, + content: params.content, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + blogPostId: data.id ?? '', + title: data.title ?? '', + status: data.status ?? null, + spaceId: data.spaceId ?? null, + version: data.version ?? null, + url: data._links?.webui ?? '', + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + blogPostId: { type: 'string', description: 'Updated blog post ID' }, + title: { type: 'string', description: 'Blog post title' }, + status: { type: 'string', description: 'Blog post status', optional: true }, + spaceId: { type: 'string', description: 'Space ID', optional: true }, + version: { type: 'json', description: 'Version information', optional: true }, + url: { type: 'string', description: 'URL to view the blog post' }, + }, +} diff --git a/apps/sim/tools/confluence/update_space.ts b/apps/sim/tools/confluence/update_space.ts new file mode 100644 index 0000000000..c1cc6bd6db --- /dev/null +++ b/apps/sim/tools/confluence/update_space.ts @@ -0,0 +1,131 @@ +import { SPACE_DESCRIPTION_OUTPUT_PROPERTIES, TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceUpdateSpaceParams { + accessToken: string + domain: string + spaceId: string + name?: string + description?: string + cloudId?: string +} + +export interface ConfluenceUpdateSpaceResponse { + success: boolean + output: { + ts: string + spaceId: string + name: string + key: string + type: string + status: string + url: string + description: { value: string; representation: string } | null + } +} + +export const confluenceUpdateSpaceTool: ToolConfig< + ConfluenceUpdateSpaceParams, + ConfluenceUpdateSpaceResponse +> = { + id: 'confluence_update_space', + name: 'Confluence Update Space', + description: 'Update a Confluence space name or description.', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + spaceId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the space to update', + }, + name: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New name for the space', + }, + description: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'New description for the space', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/space', + method: 'PUT', + headers: (params: ConfluenceUpdateSpaceParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceUpdateSpaceParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + spaceId: params.spaceId, + name: params.name, + description: params.description, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + spaceId: data.id ?? '', + name: data.name ?? '', + key: data.key ?? '', + type: data.type ?? '', + status: data.status ?? '', + url: data._links?.webui ?? '', + description: data.description ?? null, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + spaceId: { type: 'string', description: 'Updated space ID' }, + name: { type: 'string', description: 'Space name' }, + key: { type: 'string', description: 'Space key' }, + type: { type: 'string', description: 'Space type' }, + status: { type: 'string', description: 'Space status' }, + url: { type: 'string', description: 'URL to view the space' }, + description: { + type: 'object', + description: 'Space description', + properties: SPACE_DESCRIPTION_OUTPUT_PROPERTIES, + optional: true, + }, + }, +} diff --git a/apps/sim/tools/confluence/update_task.ts b/apps/sim/tools/confluence/update_task.ts new file mode 100644 index 0000000000..26e4134b55 --- /dev/null +++ b/apps/sim/tools/confluence/update_task.ts @@ -0,0 +1,138 @@ +import { TIMESTAMP_OUTPUT } from '@/tools/confluence/types' +import type { ToolConfig } from '@/tools/types' + +export interface ConfluenceUpdateTaskParams { + accessToken: string + domain: string + taskId: string + status: string + cloudId?: string +} + +export interface ConfluenceUpdateTaskResponse { + success: boolean + output: { + ts: string + id: string + localId: string | null + spaceId: string | null + pageId: string | null + blogPostId: string | null + status: string + createdBy: string | null + assignedTo: string | null + completedBy: string | null + createdAt: string | null + updatedAt: string | null + dueAt: string | null + completedAt: string | null + } +} + +export const confluenceUpdateTaskTool: ToolConfig< + ConfluenceUpdateTaskParams, + ConfluenceUpdateTaskResponse +> = { + id: 'confluence_update_task', + name: 'Confluence Update Task', + description: 'Update the status of a Confluence inline task (complete or incomplete).', + version: '1.0.0', + + oauth: { + required: true, + provider: 'confluence', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + taskId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the task to update', + }, + status: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'New status for the task (complete or incomplete)', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, + }, + + request: { + url: () => '/api/tools/confluence/tasks', + method: 'POST', + headers: (params: ConfluenceUpdateTaskParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceUpdateTaskParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + action: 'update', + taskId: params.taskId, + status: params.status, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + const task = data.task || data + return { + success: true, + output: { + ts: new Date().toISOString(), + id: task.id ?? '', + localId: task.localId ?? null, + spaceId: task.spaceId ?? null, + pageId: task.pageId ?? null, + blogPostId: task.blogPostId ?? null, + status: task.status ?? '', + createdBy: task.createdBy ?? null, + assignedTo: task.assignedTo ?? null, + completedBy: task.completedBy ?? null, + createdAt: task.createdAt ?? null, + updatedAt: task.updatedAt ?? null, + dueAt: task.dueAt ?? null, + completedAt: task.completedAt ?? null, + }, + } + }, + + outputs: { + ts: TIMESTAMP_OUTPUT, + id: { type: 'string', description: 'Task ID' }, + localId: { type: 'string', description: 'Local task ID', optional: true }, + spaceId: { type: 'string', description: 'Space ID', optional: true }, + pageId: { type: 'string', description: 'Page ID', optional: true }, + blogPostId: { type: 'string', description: 'Blog post ID', optional: true }, + status: { type: 'string', description: 'Updated task status' }, + createdBy: { type: 'string', description: 'Creator account ID', optional: true }, + assignedTo: { type: 'string', description: 'Assignee account ID', optional: true }, + completedBy: { type: 'string', description: 'Completer account ID', optional: true }, + createdAt: { type: 'string', description: 'Creation timestamp', optional: true }, + updatedAt: { type: 'string', description: 'Last update timestamp', optional: true }, + dueAt: { type: 'string', description: 'Due date', optional: true }, + completedAt: { type: 'string', description: 'Completion timestamp', optional: true }, + }, +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 22083eb2d6..fccb73665a 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -190,17 +190,24 @@ import { confluenceCreateCommentTool, confluenceCreatePagePropertyTool, confluenceCreatePageTool, + confluenceCreateSpaceTool, + confluenceCreateSpacePropertyTool, confluenceDeleteAttachmentTool, + confluenceDeleteBlogPostTool, confluenceDeleteCommentTool, confluenceDeleteLabelTool, confluenceDeletePagePropertyTool, confluenceDeletePageTool, + confluenceDeleteSpaceTool, + confluenceDeleteSpacePropertyTool, confluenceGetBlogPostTool, confluenceGetPageAncestorsTool, confluenceGetPageChildrenTool, + confluenceGetPageDescendantsTool, confluenceGetPagesByLabelTool, confluenceGetPageVersionTool, confluenceGetSpaceTool, + confluenceGetTaskTool, confluenceGetUserTool, confluenceListAttachmentsTool, confluenceListBlogPostsInSpaceTool, @@ -211,11 +218,17 @@ import { confluenceListPagesInSpaceTool, confluenceListPageVersionsTool, confluenceListSpaceLabelsTool, + confluenceListSpacePermissionsTool, + confluenceListSpacePropertiesTool, confluenceListSpacesTool, + confluenceListTasksTool, confluenceRetrieveTool, confluenceSearchInSpaceTool, confluenceSearchTool, + confluenceUpdateBlogPostTool, confluenceUpdateCommentTool, + confluenceUpdateSpaceTool, + confluenceUpdateTaskTool, confluenceUpdateTool, confluenceUploadAttachmentTool, } from '@/tools/confluence' @@ -2998,9 +3011,22 @@ export const tools: Record = { confluence_list_space_labels: confluenceListSpaceLabelsTool, confluence_delete_label: confluenceDeleteLabelTool, confluence_delete_page_property: confluenceDeletePagePropertyTool, + confluence_get_page_descendants: confluenceGetPageDescendantsTool, confluence_get_space: confluenceGetSpaceTool, + confluence_create_space: confluenceCreateSpaceTool, + confluence_update_space: confluenceUpdateSpaceTool, + confluence_delete_space: confluenceDeleteSpaceTool, confluence_get_user: confluenceGetUserTool, confluence_list_spaces: confluenceListSpacesTool, + confluence_update_blogpost: confluenceUpdateBlogPostTool, + confluence_delete_blogpost: confluenceDeleteBlogPostTool, + confluence_list_tasks: confluenceListTasksTool, + confluence_get_task: confluenceGetTaskTool, + confluence_update_task: confluenceUpdateTaskTool, + confluence_list_space_permissions: confluenceListSpacePermissionsTool, + confluence_list_space_properties: confluenceListSpacePropertiesTool, + confluence_create_space_property: confluenceCreateSpacePropertyTool, + confluence_delete_space_property: confluenceDeleteSpacePropertyTool, cursor_list_agents: cursorListAgentsTool, cursor_list_agents_v2: cursorListAgentsV2Tool, cursor_get_agent: cursorGetAgentTool, From 484565afb12208af3b2005324644eab973965cbe Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 25 Feb 2026 15:00:51 -0800 Subject: [PATCH 03/16] fix(confluence): add missing OAuth scopes to auth.ts provider config The OAuth authorization flow uses scopes from auth.ts, not oauth.ts. The 9 new scopes were only added to oauth.ts and the block config but not to the actual provider config in auth.ts, causing re-auth to still return tokens without the new scopes. Co-Authored-By: Claude Opus 4.6 --- apps/sim/lib/auth/auth.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index 91eb88b1d3..33c9abfa2f 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -1846,6 +1846,15 @@ export const auth = betterAuth({ 'write:content.property:confluence', 'read:hierarchical-content:confluence', 'read:content.metadata:confluence', + 'read:user:confluence', + 'read:task:confluence', + 'write:task:confluence', + 'delete:blogpost:confluence', + 'write:space:confluence', + 'delete:space:confluence', + 'read:space.property:confluence', + 'write:space.property:confluence', + 'read:space.permission:confluence', ], responseType: 'code', pkce: true, From a97ea59777b9ceae6a354f627ba4f8eb9500f03a Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 25 Feb 2026 15:09:00 -0800 Subject: [PATCH 04/16] lint --- apps/docs/components/ui/icon-mapping.ts | 12 +- apps/docs/content/docs/en/tools/meta.json | 2 +- .../api/tools/confluence/blogposts/route.ts | 6 +- .../app/api/tools/confluence/space/route.ts | 9 +- apps/sim/blocks/blocks/confluence.ts | 15 +- apps/sim/tools/confluence/get_task.ts | 182 +++++++++--------- apps/sim/tools/confluence/get_user.ts | 166 ++++++++-------- .../confluence/list_space_permissions.ts | 23 ++- apps/sim/tools/confluence/list_tasks.ts | 3 +- apps/sim/tools/registry.ts | 4 +- 10 files changed, 219 insertions(+), 203 deletions(-) diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index 7f36adb31a..5121253240 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -38,8 +38,8 @@ import { EyeIcon, FirecrawlIcon, FirefliesIcon, - GitLabIcon, GithubIcon, + GitLabIcon, GmailIcon, GongIcon, GoogleBooksIcon, @@ -72,9 +72,9 @@ import { LinearIcon, LinkedInIcon, LinkupIcon, - MailServerIcon, MailchimpIcon, MailgunIcon, + MailServerIcon, Mem0Icon, MicrosoftDataverseIcon, MicrosoftExcelIcon, @@ -107,8 +107,6 @@ import { ResendIcon, RevenueCatIcon, S3Icon, - SQSIcon, - STTIcon, SalesforceIcon, SearchIcon, SendgridIcon, @@ -120,17 +118,19 @@ import { SimilarwebIcon, SlackIcon, SmtpIcon, + SQSIcon, SshIcon, + STTIcon, StagehandIcon, StripeIcon, SupabaseIcon, - TTSIcon, TavilyIcon, TelegramIcon, TextractIcon, TinybirdIcon, TranslateIcon, TrelloIcon, + TTSIcon, TwilioIcon, TypeformIcon, UpstashIcon, @@ -141,11 +141,11 @@ import { WhatsAppIcon, WikipediaIcon, WordpressIcon, + xIcon, YouTubeIcon, ZendeskIcon, ZepIcon, ZoomIcon, - xIcon, } from '@/components/icons' type IconComponent = ComponentType> diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index ce8f8e6b28..9fc1cc577e 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -146,4 +146,4 @@ "zep", "zoom" ] -} \ No newline at end of file +} diff --git a/apps/sim/app/api/tools/confluence/blogposts/route.ts b/apps/sim/app/api/tools/confluence/blogposts/route.ts index 27ccc9bac7..e17153f942 100644 --- a/apps/sim/app/api/tools/confluence/blogposts/route.ts +++ b/apps/sim/app/api/tools/confluence/blogposts/route.ts @@ -360,8 +360,7 @@ export async function PUT(request: NextRequest) { statusText: response.statusText, error: JSON.stringify(errorData, null, 2), }) - const errorMessage = - errorData?.message || `Failed to update blog post (${response.status})` + const errorMessage = errorData?.message || `Failed to update blog post (${response.status})` return NextResponse.json({ error: errorMessage }, { status: response.status }) } @@ -425,8 +424,7 @@ export async function DELETE(request: NextRequest) { statusText: response.statusText, error: JSON.stringify(errorData, null, 2), }) - const errorMessage = - errorData?.message || `Failed to delete blog post (${response.status})` + const errorMessage = errorData?.message || `Failed to delete blog post (${response.status})` return NextResponse.json({ error: errorMessage }, { status: response.status }) } diff --git a/apps/sim/app/api/tools/confluence/space/route.ts b/apps/sim/app/api/tools/confluence/space/route.ts index 5ab71dbc1a..874db2ba0d 100644 --- a/apps/sim/app/api/tools/confluence/space/route.ts +++ b/apps/sim/app/api/tools/confluence/space/route.ts @@ -142,8 +142,7 @@ export async function POST(request: NextRequest) { statusText: response.statusText, error: JSON.stringify(errorData, null, 2), }) - const errorMessage = - errorData?.message || `Failed to create space (${response.status})` + const errorMessage = errorData?.message || `Failed to create space (${response.status})` return NextResponse.json({ error: errorMessage }, { status: response.status }) } @@ -223,8 +222,7 @@ export async function PUT(request: NextRequest) { statusText: response.statusText, error: JSON.stringify(errorData, null, 2), }) - const errorMessage = - errorData?.message || `Failed to update space (${response.status})` + const errorMessage = errorData?.message || `Failed to update space (${response.status})` return NextResponse.json({ error: errorMessage }, { status: response.status }) } @@ -296,8 +294,7 @@ export async function DELETE(request: NextRequest) { statusText: response.statusText, error: JSON.stringify(errorData, null, 2), }) - const errorMessage = - errorData?.message || `Failed to delete space (${response.status})` + const errorMessage = errorData?.message || `Failed to delete space (${response.status})` return NextResponse.json({ error: errorMessage }, { status: response.status }) } diff --git a/apps/sim/blocks/blocks/confluence.ts b/apps/sim/blocks/blocks/confluence.ts index 1ef03831f3..8ebc3c459c 100644 --- a/apps/sim/blocks/blocks/confluence.ts +++ b/apps/sim/blocks/blocks/confluence.ts @@ -675,7 +675,10 @@ export const ConfluenceV2Block: BlockConfig = { type: 'short-input', placeholder: 'Enter blog post ID', required: true, - condition: { field: 'operation', value: ['get_blogpost', 'update_blogpost', 'delete_blogpost'] }, + condition: { + field: 'operation', + value: ['get_blogpost', 'update_blogpost', 'delete_blogpost'], + }, }, { id: 'versionNumber', @@ -794,14 +797,20 @@ export const ConfluenceV2Block: BlockConfig = { title: 'Title', type: 'short-input', placeholder: 'Enter title', - condition: { field: 'operation', value: ['create', 'update', 'create_blogpost', 'update_blogpost', 'update_space'] }, + condition: { + field: 'operation', + value: ['create', 'update', 'create_blogpost', 'update_blogpost', 'update_space'], + }, }, { id: 'content', title: 'Content', type: 'long-input', placeholder: 'Enter content', - condition: { field: 'operation', value: ['create', 'update', 'create_blogpost', 'update_blogpost'] }, + condition: { + field: 'operation', + value: ['create', 'update', 'create_blogpost', 'update_blogpost'], + }, }, { id: 'parentId', diff --git a/apps/sim/tools/confluence/get_task.ts b/apps/sim/tools/confluence/get_task.ts index fddfaa9012..a8b16e44bd 100644 --- a/apps/sim/tools/confluence/get_task.ts +++ b/apps/sim/tools/confluence/get_task.ts @@ -28,102 +28,100 @@ export interface ConfluenceGetTaskResponse { } } -export const confluenceGetTaskTool: ToolConfig< - ConfluenceGetTaskParams, - ConfluenceGetTaskResponse -> = { - id: 'confluence_get_task', - name: 'Confluence Get Task', - description: 'Get a specific Confluence inline task by ID.', - version: '1.0.0', +export const confluenceGetTaskTool: ToolConfig = + { + id: 'confluence_get_task', + name: 'Confluence Get Task', + description: 'Get a specific Confluence inline task by ID.', + version: '1.0.0', - oauth: { - required: true, - provider: 'confluence', - }, - - params: { - accessToken: { - type: 'string', - required: true, - visibility: 'hidden', - description: 'OAuth access token for Confluence', - }, - domain: { - type: 'string', - required: true, - visibility: 'user-only', - description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', - }, - taskId: { - type: 'string', + oauth: { required: true, - visibility: 'user-or-llm', - description: 'The ID of the task to retrieve', + provider: 'confluence', }, - cloudId: { - type: 'string', - required: false, - visibility: 'user-only', - description: - 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + taskId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The ID of the task to retrieve', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, }, - }, - request: { - url: () => '/api/tools/confluence/tasks', - method: 'POST', - headers: (params: ConfluenceGetTaskParams) => ({ - Accept: 'application/json', - 'Content-Type': 'application/json', - Authorization: `Bearer ${params.accessToken}`, - }), - body: (params: ConfluenceGetTaskParams) => ({ - domain: params.domain, - accessToken: params.accessToken, - cloudId: params.cloudId, - taskId: params.taskId, - }), - }, + request: { + url: () => '/api/tools/confluence/tasks', + method: 'POST', + headers: (params: ConfluenceGetTaskParams) => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), + body: (params: ConfluenceGetTaskParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + cloudId: params.cloudId, + taskId: params.taskId, + }), + }, - transformResponse: async (response: Response) => { - const data = await response.json() - const task = data.task || data - return { - success: true, - output: { - ts: new Date().toISOString(), - id: task.id ?? '', - localId: task.localId ?? null, - spaceId: task.spaceId ?? null, - pageId: task.pageId ?? null, - blogPostId: task.blogPostId ?? null, - status: task.status ?? '', - createdBy: task.createdBy ?? null, - assignedTo: task.assignedTo ?? null, - completedBy: task.completedBy ?? null, - createdAt: task.createdAt ?? null, - updatedAt: task.updatedAt ?? null, - dueAt: task.dueAt ?? null, - completedAt: task.completedAt ?? null, - }, - } - }, + transformResponse: async (response: Response) => { + const data = await response.json() + const task = data.task || data + return { + success: true, + output: { + ts: new Date().toISOString(), + id: task.id ?? '', + localId: task.localId ?? null, + spaceId: task.spaceId ?? null, + pageId: task.pageId ?? null, + blogPostId: task.blogPostId ?? null, + status: task.status ?? '', + createdBy: task.createdBy ?? null, + assignedTo: task.assignedTo ?? null, + completedBy: task.completedBy ?? null, + createdAt: task.createdAt ?? null, + updatedAt: task.updatedAt ?? null, + dueAt: task.dueAt ?? null, + completedAt: task.completedAt ?? null, + }, + } + }, - outputs: { - ts: TIMESTAMP_OUTPUT, - id: { type: 'string', description: 'Task ID' }, - localId: { type: 'string', description: 'Local task ID', optional: true }, - spaceId: { type: 'string', description: 'Space ID', optional: true }, - pageId: { type: 'string', description: 'Page ID', optional: true }, - blogPostId: { type: 'string', description: 'Blog post ID', optional: true }, - status: { type: 'string', description: 'Task status (complete or incomplete)' }, - createdBy: { type: 'string', description: 'Creator account ID', optional: true }, - assignedTo: { type: 'string', description: 'Assignee account ID', optional: true }, - completedBy: { type: 'string', description: 'Completer account ID', optional: true }, - createdAt: { type: 'string', description: 'Creation timestamp', optional: true }, - updatedAt: { type: 'string', description: 'Last update timestamp', optional: true }, - dueAt: { type: 'string', description: 'Due date', optional: true }, - completedAt: { type: 'string', description: 'Completion timestamp', optional: true }, - }, -} + outputs: { + ts: TIMESTAMP_OUTPUT, + id: { type: 'string', description: 'Task ID' }, + localId: { type: 'string', description: 'Local task ID', optional: true }, + spaceId: { type: 'string', description: 'Space ID', optional: true }, + pageId: { type: 'string', description: 'Page ID', optional: true }, + blogPostId: { type: 'string', description: 'Blog post ID', optional: true }, + status: { type: 'string', description: 'Task status (complete or incomplete)' }, + createdBy: { type: 'string', description: 'Creator account ID', optional: true }, + assignedTo: { type: 'string', description: 'Assignee account ID', optional: true }, + completedBy: { type: 'string', description: 'Completer account ID', optional: true }, + createdAt: { type: 'string', description: 'Creation timestamp', optional: true }, + updatedAt: { type: 'string', description: 'Last update timestamp', optional: true }, + dueAt: { type: 'string', description: 'Due date', optional: true }, + completedAt: { type: 'string', description: 'Completion timestamp', optional: true }, + }, + } diff --git a/apps/sim/tools/confluence/get_user.ts b/apps/sim/tools/confluence/get_user.ts index 6935ac9a2d..15a6fdef3a 100644 --- a/apps/sim/tools/confluence/get_user.ts +++ b/apps/sim/tools/confluence/get_user.ts @@ -21,98 +21,96 @@ export interface ConfluenceGetUserResponse { } } -export const confluenceGetUserTool: ToolConfig< - ConfluenceGetUserParams, - ConfluenceGetUserResponse -> = { - id: 'confluence_get_user', - name: 'Confluence Get User', - description: 'Get a Confluence user\'s display name and profile info by their account ID.', - version: '1.0.0', +export const confluenceGetUserTool: ToolConfig = + { + id: 'confluence_get_user', + name: 'Confluence Get User', + description: "Get a Confluence user's display name and profile info by their account ID.", + version: '1.0.0', - oauth: { - required: true, - provider: 'confluence', - }, - - params: { - accessToken: { - type: 'string', - required: true, - visibility: 'hidden', - description: 'OAuth access token for Confluence', - }, - domain: { - type: 'string', + oauth: { required: true, - visibility: 'user-only', - description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + provider: 'confluence', }, - accountId: { - type: 'string', - required: true, - visibility: 'user-or-llm', - description: 'The Atlassian account ID of the user to look up', + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for Confluence', + }, + domain: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Your Confluence domain (e.g., yourcompany.atlassian.net)', + }, + accountId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The Atlassian account ID of the user to look up', + }, + cloudId: { + type: 'string', + required: false, + visibility: 'user-only', + description: + 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + }, }, - cloudId: { - type: 'string', - required: false, - visibility: 'user-only', - description: - 'Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain.', + + request: { + url: (params: ConfluenceGetUserParams) => { + const query = new URLSearchParams({ + domain: params.domain, + accessToken: params.accessToken, + accountId: params.accountId?.trim(), + }) + if (params.cloudId) { + query.set('cloudId', params.cloudId) + } + return `/api/tools/confluence/user?${query.toString()}` + }, + method: 'GET', + headers: (params: ConfluenceGetUserParams) => ({ + Accept: 'application/json', + Authorization: `Bearer ${params.accessToken}`, + }), }, - }, - request: { - url: (params: ConfluenceGetUserParams) => { - const query = new URLSearchParams({ - domain: params.domain, - accessToken: params.accessToken, - accountId: params.accountId?.trim(), - }) - if (params.cloudId) { - query.set('cloudId', params.cloudId) + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + ts: new Date().toISOString(), + accountId: data.accountId ?? '', + displayName: data.displayName ?? '', + email: data.email ?? null, + accountType: data.accountType ?? null, + profilePicture: data.profilePicture?.path ?? null, + publicName: data.publicName ?? null, + }, } - return `/api/tools/confluence/user?${query.toString()}` }, - method: 'GET', - headers: (params: ConfluenceGetUserParams) => ({ - Accept: 'application/json', - Authorization: `Bearer ${params.accessToken}`, - }), - }, - transformResponse: async (response: Response) => { - const data = await response.json() - return { - success: true, - output: { - ts: new Date().toISOString(), - accountId: data.accountId ?? '', - displayName: data.displayName ?? '', - email: data.email ?? null, - accountType: data.accountType ?? null, - profilePicture: data.profilePicture?.path ?? null, - publicName: data.publicName ?? null, + outputs: { + ts: TIMESTAMP_OUTPUT, + accountId: { type: 'string', description: 'Atlassian account ID of the user' }, + displayName: { type: 'string', description: 'Display name of the user' }, + email: { type: 'string', description: 'Email address of the user', optional: true }, + accountType: { + type: 'string', + description: 'Account type (e.g., atlassian, app, customer)', + optional: true, }, - } - }, - - outputs: { - ts: TIMESTAMP_OUTPUT, - accountId: { type: 'string', description: 'Atlassian account ID of the user' }, - displayName: { type: 'string', description: 'Display name of the user' }, - email: { type: 'string', description: 'Email address of the user', optional: true }, - accountType: { - type: 'string', - description: 'Account type (e.g., atlassian, app, customer)', - optional: true, - }, - profilePicture: { - type: 'string', - description: 'Path to the user profile picture', - optional: true, + profilePicture: { + type: 'string', + description: 'Path to the user profile picture', + optional: true, + }, + publicName: { type: 'string', description: 'Public name of the user', optional: true }, }, - publicName: { type: 'string', description: 'Public name of the user', optional: true }, - }, -} + } diff --git a/apps/sim/tools/confluence/list_space_permissions.ts b/apps/sim/tools/confluence/list_space_permissions.ts index f47c559dbd..3d8fe00f2b 100644 --- a/apps/sim/tools/confluence/list_space_permissions.ts +++ b/apps/sim/tools/confluence/list_space_permissions.ts @@ -122,12 +122,27 @@ export const confluenceListSpacePermissionsTool: ToolConfig< type: 'object', properties: { id: { type: 'string', description: 'Permission ID' }, - principalType: { type: 'string', description: 'Principal type (user, group, role)', optional: true }, + principalType: { + type: 'string', + description: 'Principal type (user, group, role)', + optional: true, + }, principalId: { type: 'string', description: 'Principal ID', optional: true }, - operationKey: { type: 'string', description: 'Operation key (read, create, delete, etc.)', optional: true }, - operationTargetType: { type: 'string', description: 'Target type (page, blogpost, space, etc.)', optional: true }, + operationKey: { + type: 'string', + description: 'Operation key (read, create, delete, etc.)', + optional: true, + }, + operationTargetType: { + type: 'string', + description: 'Target type (page, blogpost, space, etc.)', + optional: true, + }, anonymousAccess: { type: 'boolean', description: 'Whether anonymous access is allowed' }, - unlicensedAccess: { type: 'boolean', description: 'Whether unlicensed access is allowed' }, + unlicensedAccess: { + type: 'boolean', + description: 'Whether unlicensed access is allowed', + }, }, }, }, diff --git a/apps/sim/tools/confluence/list_tasks.ts b/apps/sim/tools/confluence/list_tasks.ts index 43edf00eb3..a6773de4b7 100644 --- a/apps/sim/tools/confluence/list_tasks.ts +++ b/apps/sim/tools/confluence/list_tasks.ts @@ -42,7 +42,8 @@ export const confluenceListTasksTool: ToolConfig< > = { id: 'confluence_list_tasks', name: 'Confluence List Tasks', - description: 'List inline tasks from Confluence. Optionally filter by page, space, assignee, or status.', + description: + 'List inline tasks from Confluence. Optionally filter by page, space, assignee, or status.', version: '1.0.0', oauth: { diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index fccb73665a..d77d0761f0 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -190,16 +190,16 @@ import { confluenceCreateCommentTool, confluenceCreatePagePropertyTool, confluenceCreatePageTool, - confluenceCreateSpaceTool, confluenceCreateSpacePropertyTool, + confluenceCreateSpaceTool, confluenceDeleteAttachmentTool, confluenceDeleteBlogPostTool, confluenceDeleteCommentTool, confluenceDeleteLabelTool, confluenceDeletePagePropertyTool, confluenceDeletePageTool, - confluenceDeleteSpaceTool, confluenceDeleteSpacePropertyTool, + confluenceDeleteSpaceTool, confluenceGetBlogPostTool, confluenceGetPageAncestorsTool, confluenceGetPageChildrenTool, From 1bb6276c353dcd3451c79def06552b5b70e4ad3f Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 25 Feb 2026 15:16:47 -0800 Subject: [PATCH 05/16] fix(confluence): fix truncated get_user tool description in docs Remove apostrophe from description that caused MDX generation to truncate at the escape character. Co-Authored-By: Claude Opus 4.6 --- apps/docs/components/ui/icon-mapping.ts | 12 ++++++------ apps/docs/content/docs/en/tools/confluence.mdx | 2 +- apps/docs/content/docs/en/tools/meta.json | 2 +- apps/sim/tools/confluence/get_user.ts | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index 5121253240..7f36adb31a 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -38,8 +38,8 @@ import { EyeIcon, FirecrawlIcon, FirefliesIcon, - GithubIcon, GitLabIcon, + GithubIcon, GmailIcon, GongIcon, GoogleBooksIcon, @@ -72,9 +72,9 @@ import { LinearIcon, LinkedInIcon, LinkupIcon, + MailServerIcon, MailchimpIcon, MailgunIcon, - MailServerIcon, Mem0Icon, MicrosoftDataverseIcon, MicrosoftExcelIcon, @@ -107,6 +107,8 @@ import { ResendIcon, RevenueCatIcon, S3Icon, + SQSIcon, + STTIcon, SalesforceIcon, SearchIcon, SendgridIcon, @@ -118,19 +120,17 @@ import { SimilarwebIcon, SlackIcon, SmtpIcon, - SQSIcon, SshIcon, - STTIcon, StagehandIcon, StripeIcon, SupabaseIcon, + TTSIcon, TavilyIcon, TelegramIcon, TextractIcon, TinybirdIcon, TranslateIcon, TrelloIcon, - TTSIcon, TwilioIcon, TypeformIcon, UpstashIcon, @@ -141,11 +141,11 @@ import { WhatsAppIcon, WikipediaIcon, WordpressIcon, - xIcon, YouTubeIcon, ZendeskIcon, ZepIcon, ZoomIcon, + xIcon, } from '@/components/icons' type IconComponent = ComponentType> diff --git a/apps/docs/content/docs/en/tools/confluence.mdx b/apps/docs/content/docs/en/tools/confluence.mdx index 598e1e9d38..6e5f3707e3 100644 --- a/apps/docs/content/docs/en/tools/confluence.mdx +++ b/apps/docs/content/docs/en/tools/confluence.mdx @@ -1399,7 +1399,7 @@ Delete a Confluence blog post. ### `confluence_get_user` -Get a Confluence user\ +Get display name and profile info for a Confluence user by account ID. #### Input diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index 9fc1cc577e..ce8f8e6b28 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -146,4 +146,4 @@ "zep", "zoom" ] -} +} \ No newline at end of file diff --git a/apps/sim/tools/confluence/get_user.ts b/apps/sim/tools/confluence/get_user.ts index 15a6fdef3a..e87def8a64 100644 --- a/apps/sim/tools/confluence/get_user.ts +++ b/apps/sim/tools/confluence/get_user.ts @@ -25,7 +25,7 @@ export const confluenceGetUserTool: ToolConfig Date: Wed, 25 Feb 2026 15:22:32 -0800 Subject: [PATCH 06/16] fix(confluence): address PR review feedback - Move get_user from GET to POST to avoid exposing access token in URL - Add 400 validation for missing params in space-properties create/delete - Add null check for blog post version before update to prevent TypeError Co-Authored-By: Claude Opus 4.6 --- .../api/tools/confluence/blogposts/route.ts | 8 +++++++ .../confluence/space-properties/route.ts | 15 +++++++++++++ .../app/api/tools/confluence/user/route.ts | 9 +++----- apps/sim/tools/confluence/get_user.ts | 21 ++++++++----------- 4 files changed, 35 insertions(+), 18 deletions(-) diff --git a/apps/sim/app/api/tools/confluence/blogposts/route.ts b/apps/sim/app/api/tools/confluence/blogposts/route.ts index e17153f942..7660390930 100644 --- a/apps/sim/app/api/tools/confluence/blogposts/route.ts +++ b/apps/sim/app/api/tools/confluence/blogposts/route.ts @@ -330,6 +330,14 @@ export async function PUT(request: NextRequest) { } const currentPost = await currentResponse.json() + + if (!currentPost.version?.number) { + return NextResponse.json( + { error: 'Unable to determine current blog post version' }, + { status: 422 } + ) + } + const currentVersion = currentPost.version.number const updateBody: Record = { diff --git a/apps/sim/app/api/tools/confluence/space-properties/route.ts b/apps/sim/app/api/tools/confluence/space-properties/route.ts index 0bceb68c5b..dce10a74cb 100644 --- a/apps/sim/app/api/tools/confluence/space-properties/route.ts +++ b/apps/sim/app/api/tools/confluence/space-properties/route.ts @@ -60,6 +60,21 @@ export async function POST(request: NextRequest) { const baseUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}/properties` + // Validate required params for specific actions + if (action === 'delete' && !propertyId) { + return NextResponse.json( + { error: 'Property ID is required for delete action' }, + { status: 400 } + ) + } + + if (action === 'create' && !key) { + return NextResponse.json( + { error: 'Property key is required for create action' }, + { status: 400 } + ) + } + // Delete a property if (action === 'delete' && propertyId) { const propertyIdValidation = validateAlphanumericId(propertyId, 'propertyId', 255) diff --git a/apps/sim/app/api/tools/confluence/user/route.ts b/apps/sim/app/api/tools/confluence/user/route.ts index d8523c0724..0cb0c3b1b0 100644 --- a/apps/sim/app/api/tools/confluence/user/route.ts +++ b/apps/sim/app/api/tools/confluence/user/route.ts @@ -12,18 +12,15 @@ export const dynamic = 'force-dynamic' * Get a Confluence user by account ID. * Uses GET /wiki/rest/api/user?accountId={accountId} */ -export async function GET(request: NextRequest) { +export async function POST(request: NextRequest) { try { const auth = await checkSessionOrInternalAuth(request) if (!auth.success || !auth.userId) { return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) } - const { searchParams } = new URL(request.url) - const domain = searchParams.get('domain') - const accessToken = searchParams.get('accessToken') - const accountId = searchParams.get('accountId') - const providedCloudId = searchParams.get('cloudId') + const body = await request.json() + const { domain, accessToken, accountId, cloudId: providedCloudId } = body if (!domain) { return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) diff --git a/apps/sim/tools/confluence/get_user.ts b/apps/sim/tools/confluence/get_user.ts index e87def8a64..2304836135 100644 --- a/apps/sim/tools/confluence/get_user.ts +++ b/apps/sim/tools/confluence/get_user.ts @@ -62,22 +62,19 @@ export const confluenceGetUserTool: ToolConfig { - const query = new URLSearchParams({ - domain: params.domain, - accessToken: params.accessToken, - accountId: params.accountId?.trim(), - }) - if (params.cloudId) { - query.set('cloudId', params.cloudId) - } - return `/api/tools/confluence/user?${query.toString()}` - }, - method: 'GET', + url: () => '/api/tools/confluence/user', + method: 'POST', headers: (params: ConfluenceGetUserParams) => ({ Accept: 'application/json', + 'Content-Type': 'application/json', Authorization: `Bearer ${params.accessToken}`, }), + body: (params: ConfluenceGetUserParams) => ({ + domain: params.domain, + accessToken: params.accessToken, + accountId: params.accountId?.trim(), + cloudId: params.cloudId, + }), }, transformResponse: async (response: Response) => { From 7f80c813796e4994cc1b16f546a11850954a1592 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 25 Feb 2026 15:30:29 -0800 Subject: [PATCH 07/16] feat(confluence): add missing response fields for descendants and tasks - Add type and depth fields to page descendants (from Confluence API) - Add body field (storage format) to task list/get/update responses Co-Authored-By: Claude Opus 4.6 --- apps/docs/content/docs/en/tools/confluence.mdx | 5 +++++ apps/sim/app/api/tools/confluence/page-descendants/route.ts | 2 ++ apps/sim/app/api/tools/confluence/tasks/route.ts | 3 +++ apps/sim/tools/confluence/get_page_descendants.ts | 4 ++++ apps/sim/tools/confluence/get_task.ts | 3 +++ apps/sim/tools/confluence/list_tasks.ts | 2 ++ apps/sim/tools/confluence/update_task.ts | 3 +++ 7 files changed, 22 insertions(+) diff --git a/apps/docs/content/docs/en/tools/confluence.mdx b/apps/docs/content/docs/en/tools/confluence.mdx index 6e5f3707e3..5ee4ad3dc6 100644 --- a/apps/docs/content/docs/en/tools/confluence.mdx +++ b/apps/docs/content/docs/en/tools/confluence.mdx @@ -1243,10 +1243,12 @@ Get all descendants of a Confluence page recursively. | `descendants` | array | Array of descendant pages | | ↳ `id` | string | Page ID | | ↳ `title` | string | Page title | +| ↳ `type` | string | Content type \(page, whiteboard, database, etc.\) | | ↳ `status` | string | Page status | | ↳ `spaceId` | string | Space ID | | ↳ `parentId` | string | Parent page ID | | ↳ `childPosition` | number | Position among siblings | +| ↳ `depth` | number | Depth in the hierarchy | | `pageId` | string | Parent page ID | | `nextCursor` | string | Cursor for fetching the next page of results | @@ -1279,6 +1281,7 @@ List inline tasks from Confluence. Optionally filter by page, space, assignee, o | ↳ `pageId` | string | Page ID | | ↳ `blogPostId` | string | Blog post ID | | ↳ `status` | string | Task status \(complete or incomplete\) | +| ↳ `body` | string | Task body content in storage format | | ↳ `createdBy` | string | Creator account ID | | ↳ `assignedTo` | string | Assignee account ID | | ↳ `completedBy` | string | Completer account ID | @@ -1311,6 +1314,7 @@ Get a specific Confluence inline task by ID. | `pageId` | string | Page ID | | `blogPostId` | string | Blog post ID | | `status` | string | Task status \(complete or incomplete\) | +| `body` | string | Task body content in storage format | | `createdBy` | string | Creator account ID | | `assignedTo` | string | Assignee account ID | | `completedBy` | string | Completer account ID | @@ -1343,6 +1347,7 @@ Update the status of a Confluence inline task (complete or incomplete). | `pageId` | string | Page ID | | `blogPostId` | string | Blog post ID | | `status` | string | Updated task status | +| `body` | string | Task body content in storage format | | `createdBy` | string | Creator account ID | | `assignedTo` | string | Assignee account ID | | `completedBy` | string | Completer account ID | diff --git a/apps/sim/app/api/tools/confluence/page-descendants/route.ts b/apps/sim/app/api/tools/confluence/page-descendants/route.ts index 1789b36b58..ecdc500591 100644 --- a/apps/sim/app/api/tools/confluence/page-descendants/route.ts +++ b/apps/sim/app/api/tools/confluence/page-descendants/route.ts @@ -82,10 +82,12 @@ export async function POST(request: NextRequest) { const descendants = (data.results || []).map((page: any) => ({ id: page.id, title: page.title, + type: page.type ?? null, status: page.status ?? null, spaceId: page.spaceId ?? null, parentId: page.parentId ?? null, childPosition: page.childPosition ?? null, + depth: page.depth ?? null, })) return NextResponse.json({ diff --git a/apps/sim/app/api/tools/confluence/tasks/route.ts b/apps/sim/app/api/tools/confluence/tasks/route.ts index 9a8fb60510..46031dcc4f 100644 --- a/apps/sim/app/api/tools/confluence/tasks/route.ts +++ b/apps/sim/app/api/tools/confluence/tasks/route.ts @@ -113,6 +113,7 @@ export async function POST(request: NextRequest) { pageId: data.pageId ?? null, blogPostId: data.blogPostId ?? null, status: data.status, + body: data.body?.storage?.value ?? null, createdBy: data.createdBy ?? null, assignedTo: data.assignedTo ?? null, completedBy: data.completedBy ?? null, @@ -163,6 +164,7 @@ export async function POST(request: NextRequest) { pageId: data.pageId ?? null, blogPostId: data.blogPostId ?? null, status: data.status, + body: data.body?.storage?.value ?? null, createdBy: data.createdBy ?? null, assignedTo: data.assignedTo ?? null, completedBy: data.completedBy ?? null, @@ -216,6 +218,7 @@ export async function POST(request: NextRequest) { pageId: task.pageId ?? null, blogPostId: task.blogPostId ?? null, status: task.status, + body: task.body?.storage?.value ?? null, createdBy: task.createdBy ?? null, assignedTo: task.assignedTo ?? null, completedBy: task.completedBy ?? null, diff --git a/apps/sim/tools/confluence/get_page_descendants.ts b/apps/sim/tools/confluence/get_page_descendants.ts index 3caf48592b..f29580a5ba 100644 --- a/apps/sim/tools/confluence/get_page_descendants.ts +++ b/apps/sim/tools/confluence/get_page_descendants.ts @@ -17,10 +17,12 @@ export interface ConfluenceGetPageDescendantsResponse { descendants: Array<{ id: string title: string + type: string | null status: string | null spaceId: string | null parentId: string | null childPosition: number | null + depth: number | null }> pageId: string nextCursor: string | null @@ -122,10 +124,12 @@ export const confluenceGetPageDescendantsTool: ToolConfig< properties: { id: { type: 'string', description: 'Page ID' }, title: { type: 'string', description: 'Page title' }, + type: { type: 'string', description: 'Content type (page, whiteboard, database, etc.)', optional: true }, status: { type: 'string', description: 'Page status', optional: true }, spaceId: { type: 'string', description: 'Space ID', optional: true }, parentId: { type: 'string', description: 'Parent page ID', optional: true }, childPosition: { type: 'number', description: 'Position among siblings', optional: true }, + depth: { type: 'number', description: 'Depth in the hierarchy', optional: true }, }, }, }, diff --git a/apps/sim/tools/confluence/get_task.ts b/apps/sim/tools/confluence/get_task.ts index a8b16e44bd..cf0b617765 100644 --- a/apps/sim/tools/confluence/get_task.ts +++ b/apps/sim/tools/confluence/get_task.ts @@ -18,6 +18,7 @@ export interface ConfluenceGetTaskResponse { pageId: string | null blogPostId: string | null status: string + body: string | null createdBy: string | null assignedTo: string | null completedBy: string | null @@ -97,6 +98,7 @@ export const confluenceGetTaskTool: ToolConfig Date: Wed, 25 Feb 2026 15:56:25 -0800 Subject: [PATCH 08/16] lint --- apps/docs/components/ui/icon-mapping.ts | 12 ++++++------ apps/docs/content/docs/en/tools/meta.json | 2 +- apps/sim/tools/confluence/get_page_descendants.ts | 6 +++++- apps/sim/tools/confluence/list_tasks.ts | 6 +++++- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index 7f36adb31a..5121253240 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -38,8 +38,8 @@ import { EyeIcon, FirecrawlIcon, FirefliesIcon, - GitLabIcon, GithubIcon, + GitLabIcon, GmailIcon, GongIcon, GoogleBooksIcon, @@ -72,9 +72,9 @@ import { LinearIcon, LinkedInIcon, LinkupIcon, - MailServerIcon, MailchimpIcon, MailgunIcon, + MailServerIcon, Mem0Icon, MicrosoftDataverseIcon, MicrosoftExcelIcon, @@ -107,8 +107,6 @@ import { ResendIcon, RevenueCatIcon, S3Icon, - SQSIcon, - STTIcon, SalesforceIcon, SearchIcon, SendgridIcon, @@ -120,17 +118,19 @@ import { SimilarwebIcon, SlackIcon, SmtpIcon, + SQSIcon, SshIcon, + STTIcon, StagehandIcon, StripeIcon, SupabaseIcon, - TTSIcon, TavilyIcon, TelegramIcon, TextractIcon, TinybirdIcon, TranslateIcon, TrelloIcon, + TTSIcon, TwilioIcon, TypeformIcon, UpstashIcon, @@ -141,11 +141,11 @@ import { WhatsAppIcon, WikipediaIcon, WordpressIcon, + xIcon, YouTubeIcon, ZendeskIcon, ZepIcon, ZoomIcon, - xIcon, } from '@/components/icons' type IconComponent = ComponentType> diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index ce8f8e6b28..9fc1cc577e 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -146,4 +146,4 @@ "zep", "zoom" ] -} \ No newline at end of file +} diff --git a/apps/sim/tools/confluence/get_page_descendants.ts b/apps/sim/tools/confluence/get_page_descendants.ts index f29580a5ba..a9e0bc5a32 100644 --- a/apps/sim/tools/confluence/get_page_descendants.ts +++ b/apps/sim/tools/confluence/get_page_descendants.ts @@ -124,7 +124,11 @@ export const confluenceGetPageDescendantsTool: ToolConfig< properties: { id: { type: 'string', description: 'Page ID' }, title: { type: 'string', description: 'Page title' }, - type: { type: 'string', description: 'Content type (page, whiteboard, database, etc.)', optional: true }, + type: { + type: 'string', + description: 'Content type (page, whiteboard, database, etc.)', + optional: true, + }, status: { type: 'string', description: 'Page status', optional: true }, spaceId: { type: 'string', description: 'Space ID', optional: true }, parentId: { type: 'string', description: 'Parent page ID', optional: true }, diff --git a/apps/sim/tools/confluence/list_tasks.ts b/apps/sim/tools/confluence/list_tasks.ts index 8c8ea2dd8c..4f44678a89 100644 --- a/apps/sim/tools/confluence/list_tasks.ts +++ b/apps/sim/tools/confluence/list_tasks.ts @@ -157,7 +157,11 @@ export const confluenceListTasksTool: ToolConfig< pageId: { type: 'string', description: 'Page ID', optional: true }, blogPostId: { type: 'string', description: 'Blog post ID', optional: true }, status: { type: 'string', description: 'Task status (complete or incomplete)' }, - body: { type: 'string', description: 'Task body content in storage format', optional: true }, + body: { + type: 'string', + description: 'Task body content in storage format', + optional: true, + }, createdBy: { type: 'string', description: 'Creator account ID', optional: true }, assignedTo: { type: 'string', description: 'Assignee account ID', optional: true }, completedBy: { type: 'string', description: 'Completer account ID', optional: true }, From c267afd304d937e9e05ff6cf8e46cb68395abe7a Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 25 Feb 2026 16:07:30 -0800 Subject: [PATCH 09/16] fix(confluence): use validatePathSegment for Atlassian account IDs validateAlphanumericId rejects valid Atlassian account IDs that contain colons (e.g. 557058:6b9c9931-4693-49c1-8b3a-931f1af98134). Use validatePathSegment with a custom pattern allowing colons instead. Co-Authored-By: Claude Opus 4.6 --- apps/sim/app/api/tools/confluence/user/route.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/api/tools/confluence/user/route.ts b/apps/sim/app/api/tools/confluence/user/route.ts index 0cb0c3b1b0..8e05e9b810 100644 --- a/apps/sim/app/api/tools/confluence/user/route.ts +++ b/apps/sim/app/api/tools/confluence/user/route.ts @@ -1,7 +1,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' -import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' +import { validateJiraCloudId, validatePathSegment } from '@/lib/core/security/input-validation' import { getConfluenceCloudId } from '@/tools/confluence/utils' const logger = createLogger('ConfluenceUserAPI') @@ -34,7 +34,12 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Account ID is required' }, { status: 400 }) } - const accountIdValidation = validateAlphanumericId(accountId, 'accountId', 255) + // Atlassian account IDs use format like 557058:6b9c9931-4693-49c1-8b3a-931f1af98134 + const accountIdValidation = validatePathSegment(accountId, { + paramName: 'accountId', + maxLength: 255, + customPattern: /^[a-zA-Z0-9:\-]+$/, + }) if (!accountIdValidation.isValid) { return NextResponse.json({ error: accountIdValidation.error }, { status: 400 }) } From 9a2163202ee3f5741efca7361ec5716fad897b26 Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 25 Feb 2026 16:08:23 -0800 Subject: [PATCH 10/16] ran lint --- .../app/api/tools/confluence/user/route.ts | 2 +- apps/sim/socket/database/operations.ts | 49 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/api/tools/confluence/user/route.ts b/apps/sim/app/api/tools/confluence/user/route.ts index 8e05e9b810..68ea52bd1d 100644 --- a/apps/sim/app/api/tools/confluence/user/route.ts +++ b/apps/sim/app/api/tools/confluence/user/route.ts @@ -38,7 +38,7 @@ export async function POST(request: NextRequest) { const accountIdValidation = validatePathSegment(accountId, { paramName: 'accountId', maxLength: 255, - customPattern: /^[a-zA-Z0-9:\-]+$/, + customPattern: /^[a-zA-Z0-9:-]+$/, }) if (!accountIdValidation.isValid) { return NextResponse.json({ error: accountIdValidation.error }, { status: 400 }) diff --git a/apps/sim/socket/database/operations.ts b/apps/sim/socket/database/operations.ts index d677466cba..7fef6aab61 100644 --- a/apps/sim/socket/database/operations.ts +++ b/apps/sim/socket/database/operations.ts @@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger' import { and, eq, inArray, or, sql } from 'drizzle-orm' import { drizzle } from 'drizzle-orm/postgres-js' import postgres from 'postgres' +import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { env } from '@/lib/core/config/env' import { cleanupExternalWebhook } from '@/lib/webhooks/provider-subscriptions' import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils' @@ -207,6 +208,17 @@ export async function persistWorkflowOperation(workflowId: string, operation: an } }) + // Audit workflow-level lock/unlock operations + if ( + target === OPERATION_TARGETS.BLOCKS && + op === BLOCKS_OPERATIONS.BATCH_TOGGLE_LOCKED && + userId + ) { + auditWorkflowLockToggle(workflowId, userId).catch((error) => { + logger.error('Failed to audit workflow lock toggle', { error, workflowId }) + }) + } + const duration = Date.now() - startTime if (duration > 100) { logger.warn('Slow socket DB operation:', { @@ -226,6 +238,43 @@ export async function persistWorkflowOperation(workflowId: string, operation: an } } +/** + * Records an audit log entry when all blocks in a workflow are locked or unlocked. + * Only audits workflow-level transitions (all locked or all unlocked), not partial toggles. + */ +async function auditWorkflowLockToggle(workflowId: string, actorId: string): Promise { + const [wf] = await db + .select({ name: workflow.name, workspaceId: workflow.workspaceId }) + .from(workflow) + .where(eq(workflow.id, workflowId)) + + if (!wf) return + + const blocks = await db + .select({ locked: workflowBlocks.locked }) + .from(workflowBlocks) + .where(eq(workflowBlocks.workflowId, workflowId)) + + if (blocks.length === 0) return + + const allLocked = blocks.every((b) => b.locked) + const allUnlocked = blocks.every((b) => !b.locked) + + // Only audit workflow-level transitions, not partial toggles + if (!allLocked && !allUnlocked) return + + recordAudit({ + workspaceId: wf.workspaceId, + actorId, + action: allLocked ? AuditAction.WORKFLOW_LOCKED : AuditAction.WORKFLOW_UNLOCKED, + resourceType: AuditResourceType.WORKFLOW, + resourceId: workflowId, + resourceName: wf.name, + description: allLocked ? `Locked workflow "${wf.name}"` : `Unlocked workflow "${wf.name}"`, + metadata: { blockCount: blocks.length }, + }) +} + async function handleBlockOperationTx( tx: any, workflowId: string, From d0ecc0a5d14409b573b772bda0926bf638b3c698 Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 25 Feb 2026 16:31:48 -0800 Subject: [PATCH 11/16] update mock --- packages/testing/src/mocks/audit.mock.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/testing/src/mocks/audit.mock.ts b/packages/testing/src/mocks/audit.mock.ts index d0f913c7fc..d31b812b9e 100644 --- a/packages/testing/src/mocks/audit.mock.ts +++ b/packages/testing/src/mocks/audit.mock.ts @@ -86,6 +86,8 @@ export const auditMock = { WORKFLOW_DEPLOYED: 'workflow.deployed', WORKFLOW_UNDEPLOYED: 'workflow.undeployed', WORKFLOW_DUPLICATED: 'workflow.duplicated', + WORKFLOW_LOCKED: 'workflow.locked', + WORKFLOW_UNLOCKED: 'workflow.unlocked', WORKFLOW_DEPLOYMENT_ACTIVATED: 'workflow.deployment_activated', WORKFLOW_DEPLOYMENT_REVERTED: 'workflow.deployment_reverted', WORKFLOW_VARIABLES_UPDATED: 'workflow.variables_updated', From 08b0f4406e1091fa7e38b26d78a4922792241bc8 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 25 Feb 2026 16:34:11 -0800 Subject: [PATCH 12/16] fix(confluence): reject empty update body for space PUT Return 400 when neither name nor description is provided for space update, instead of sending an empty body to the Confluence API. Co-Authored-By: Claude Opus 4.6 --- apps/sim/app/api/tools/confluence/space/route.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/sim/app/api/tools/confluence/space/route.ts b/apps/sim/app/api/tools/confluence/space/route.ts index 874db2ba0d..9d0b68c172 100644 --- a/apps/sim/app/api/tools/confluence/space/route.ts +++ b/apps/sim/app/api/tools/confluence/space/route.ts @@ -197,6 +197,13 @@ export async function PUT(request: NextRequest) { const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}` + if (!name && description === undefined) { + return NextResponse.json( + { error: 'At least one of name or description is required for update' }, + { status: 400 } + ) + } + const updateBody: Record = {} if (name) updateBody.name = name if (description !== undefined) { From a75d987b296c6fee793e472c642e5acbcdcae16c Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 25 Feb 2026 16:46:07 -0800 Subject: [PATCH 13/16] fix(confluence): remove spaceId requirement for create_space and fix list_tasks pagination - Remove create_space from spaceId condition array since creating a space doesn't require a space ID input - Remove list_tasks from generic supportsCursor array so it uses its dedicated handler that correctly passes assignedTo and status filters during pagination Co-Authored-By: Claude Opus 4.6 --- apps/docs/components/ui/icon-mapping.ts | 12 ++++++------ apps/docs/content/docs/en/tools/meta.json | 2 +- apps/sim/blocks/blocks/confluence.ts | 6 ++---- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index 5121253240..7f36adb31a 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -38,8 +38,8 @@ import { EyeIcon, FirecrawlIcon, FirefliesIcon, - GithubIcon, GitLabIcon, + GithubIcon, GmailIcon, GongIcon, GoogleBooksIcon, @@ -72,9 +72,9 @@ import { LinearIcon, LinkedInIcon, LinkupIcon, + MailServerIcon, MailchimpIcon, MailgunIcon, - MailServerIcon, Mem0Icon, MicrosoftDataverseIcon, MicrosoftExcelIcon, @@ -107,6 +107,8 @@ import { ResendIcon, RevenueCatIcon, S3Icon, + SQSIcon, + STTIcon, SalesforceIcon, SearchIcon, SendgridIcon, @@ -118,19 +120,17 @@ import { SimilarwebIcon, SlackIcon, SmtpIcon, - SQSIcon, SshIcon, - STTIcon, StagehandIcon, StripeIcon, SupabaseIcon, + TTSIcon, TavilyIcon, TelegramIcon, TextractIcon, TinybirdIcon, TranslateIcon, TrelloIcon, - TTSIcon, TwilioIcon, TypeformIcon, UpstashIcon, @@ -141,11 +141,11 @@ import { WhatsAppIcon, WikipediaIcon, WordpressIcon, - xIcon, YouTubeIcon, ZendeskIcon, ZepIcon, ZoomIcon, + xIcon, } from '@/components/icons' type IconComponent = ComponentType> diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index 9fc1cc577e..ce8f8e6b28 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -146,4 +146,4 @@ "zep", "zoom" ] -} +} \ No newline at end of file diff --git a/apps/sim/blocks/blocks/confluence.ts b/apps/sim/blocks/blocks/confluence.ts index 8ebc3c459c..d092d14959 100644 --- a/apps/sim/blocks/blocks/confluence.ts +++ b/apps/sim/blocks/blocks/confluence.ts @@ -654,7 +654,6 @@ export const ConfluenceV2Block: BlockConfig = { value: [ 'create', 'get_space', - 'create_space', 'update_space', 'delete_space', 'list_pages_in_space', @@ -1293,8 +1292,8 @@ export const ConfluenceV2Block: BlockConfig = { } // Operations that support generic cursor pagination. - // get_pages_by_label and list_space_labels have dedicated handlers - // below that pass cursor along with their required params (labelId, spaceId). + // get_pages_by_label, list_space_labels, and list_tasks have dedicated handlers + // below that pass cursor along with their required params. const supportsCursor = [ 'list_attachments', 'list_spaces', @@ -1308,7 +1307,6 @@ export const ConfluenceV2Block: BlockConfig = { 'get_page_descendants', 'list_space_permissions', 'list_space_properties', - 'list_tasks', ] if (supportsCursor.includes(operation) && cursor) { From 7794820915cc6214390630df193c3b47ff98750b Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 26 Feb 2026 13:57:34 -0800 Subject: [PATCH 14/16] fix(confluence): add body-format=storage to blogpost update GET request Without this param, the Confluence v2 API does not return body content, causing the fallback to erase existing content when only updating the title. Co-Authored-By: Claude Opus 4.6 --- apps/sim/app/api/tools/confluence/blogposts/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/app/api/tools/confluence/blogposts/route.ts b/apps/sim/app/api/tools/confluence/blogposts/route.ts index 7660390930..cdfa12f593 100644 --- a/apps/sim/app/api/tools/confluence/blogposts/route.ts +++ b/apps/sim/app/api/tools/confluence/blogposts/route.ts @@ -317,7 +317,7 @@ export async function PUT(request: NextRequest) { } // Fetch current blog post to get version number - const currentUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/blogposts/${blogPostId}` + const currentUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/blogposts/${blogPostId}?body-format=storage` const currentResponse = await fetch(currentUrl, { headers: { Accept: 'application/json', From 50adfb63d5b983f00a2d6c555d3d420ed75ab89f Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 26 Feb 2026 14:00:36 -0800 Subject: [PATCH 15/16] fix(confluence): add body-format=storage to page update GET request MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same bug class as the blogpost fix — without this param, the Confluence v2 API does not return body content, causing the fallback to erase page content when only updating the title. Co-Authored-By: Claude Opus 4.6 --- apps/sim/app/api/tools/confluence/page/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/app/api/tools/confluence/page/route.ts b/apps/sim/app/api/tools/confluence/page/route.ts index 232e453a95..191ddcef6f 100644 --- a/apps/sim/app/api/tools/confluence/page/route.ts +++ b/apps/sim/app/api/tools/confluence/page/route.ts @@ -185,7 +185,7 @@ export async function PUT(request: NextRequest) { return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) } - const currentPageUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}` + const currentPageUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}?body-format=storage` const currentPageResponse = await fetch(currentPageUrl, { headers: { Accept: 'application/json', From 8b2a917507b02c0e17f6abe523f69757e6f908f2 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 26 Feb 2026 14:01:05 -0800 Subject: [PATCH 16/16] fix(confluence): fetch current space name when updating only description The Confluence v2 PUT /spaces/{id} endpoint requires the name field. When the user only provides a description update, fetch the current space first to preserve the existing name. Co-Authored-By: Claude Opus 4.6 --- .../app/api/tools/confluence/space/route.ts | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/api/tools/confluence/space/route.ts b/apps/sim/app/api/tools/confluence/space/route.ts index 9d0b68c172..ffe58f037a 100644 --- a/apps/sim/app/api/tools/confluence/space/route.ts +++ b/apps/sim/app/api/tools/confluence/space/route.ts @@ -205,7 +205,26 @@ export async function PUT(request: NextRequest) { } const updateBody: Record = {} - if (name) updateBody.name = name + + if (name) { + updateBody.name = name + } else { + const currentResponse = await fetch(url, { + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + }) + if (!currentResponse.ok) { + return NextResponse.json( + { error: `Failed to fetch current space: ${currentResponse.status}` }, + { status: currentResponse.status } + ) + } + const currentSpace = await currentResponse.json() + updateBody.name = currentSpace.name + } + if (description !== undefined) { updateBody.description = { value: description, representation: 'plain' } }