diff --git a/apps/docs/content/docs/en/tools/confluence.mdx b/apps/docs/content/docs/en/tools/confluence.mdx index 58301d2891..8ffb145f86 100644 --- a/apps/docs/content/docs/en/tools/confluence.mdx +++ b/apps/docs/content/docs/en/tools/confluence.mdx @@ -1013,6 +1013,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. @@ -1045,4 +1124,311 @@ 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 | +| ↳ `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 | + +### `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\) | +| ↳ `body` | string | Task body content in storage format | +| ↳ `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\) | +| `body` | string | Task body content in storage format | +| `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 | +| `body` | string | Task body content in storage format | +| `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 display name and profile info for a Confluence user by account ID. + +#### 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/sim/app/api/tools/confluence/blogposts/route.ts b/apps/sim/app/api/tools/confluence/blogposts/route.ts index c186d5ca58..7660390930 100644 --- a/apps/sim/app/api/tools/confluence/blogposts/route.ts +++ b/apps/sim/app/api/tools/confluence/blogposts/route.ts @@ -283,3 +283,165 @@ 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() + + 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 = { + 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..ecdc500591 --- /dev/null +++ b/apps/sim/app/api/tools/confluence/page-descendants/route.ts @@ -0,0 +1,107 @@ +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, + 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({ + 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..dce10a74cb --- /dev/null +++ b/apps/sim/app/api/tools/confluence/space-properties/route.ts @@ -0,0 +1,199 @@ +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` + + // 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) + 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..9d0b68c172 100644 --- a/apps/sim/app/api/tools/confluence/space/route.ts +++ b/apps/sim/app/api/tools/confluence/space/route.ts @@ -78,3 +78,239 @@ 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}` + + 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) { + 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..46031dcc4f --- /dev/null +++ b/apps/sim/app/api/tools/confluence/tasks/route.ts @@ -0,0 +1,244 @@ +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, + body: data.body?.storage?.value ?? null, + 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, + body: data.body?.storage?.value ?? null, + 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, + body: task.body?.storage?.value ?? null, + 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/api/tools/confluence/user/route.ts b/apps/sim/app/api/tools/confluence/user/route.ts new file mode 100644 index 0000000000..68ea52bd1d --- /dev/null +++ b/apps/sim/app/api/tools/confluence/user/route.ts @@ -0,0 +1,85 @@ +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { validateJiraCloudId, validatePathSegment } 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 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, accountId, 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 (!accountId) { + return NextResponse.json({ error: 'Account ID is required' }, { status: 400 }) + } + + // 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 }) + } + + 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/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/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx index e07976e4e6..e10bedb79b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx @@ -41,6 +41,7 @@ import { } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/hooks' import { Variables } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/variables/variables' import { useAutoLayout } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-auto-layout' +import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow' import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution' import { getWorkflowLockToggleIds } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils' import { useDeleteWorkflow, useImportWorkflow } from '@/app/workspace/[workspaceId]/w/hooks' @@ -203,6 +204,7 @@ export const Panel = memo(function Panel() { ) const currentWorkflow = activeWorkflowId ? workflows[activeWorkflowId] : null + const { isSnapshotView } = useCurrentWorkflow() /** * Mark hydration as complete on mount @@ -421,7 +423,7 @@ export const Panel = memo(function Panel() { Auto layout - {userPermissions.canAdmin && !currentWorkflow?.isSnapshotView && ( + {userPermissions.canAdmin && !isSnapshotView && ( {allBlocksLocked ? ( diff --git a/apps/sim/blocks/blocks/confluence.ts b/apps/sim/blocks/blocks/confluence.ts index c17f44b89b..d092d14959 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, @@ -414,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' }, @@ -432,7 +435,24 @@ 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' }, ], value: () => 'read', }, @@ -472,6 +492,15 @@ export const ConfluenceV2Block: BlockConfig = { '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', ], placeholder: 'Select Confluence account', required: true, @@ -507,13 +536,26 @@ 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, }, @@ -537,6 +579,7 @@ export const ConfluenceV2Block: BlockConfig = { 'get_page_version', 'list_page_properties', 'create_page_property', + 'get_page_descendants', ], }, }, @@ -553,13 +596,26 @@ 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, }, @@ -583,6 +639,7 @@ export const ConfluenceV2Block: BlockConfig = { 'get_page_version', 'list_page_properties', 'create_page_property', + 'get_page_descendants', ], }, }, @@ -597,11 +654,17 @@ export const ConfluenceV2Block: BlockConfig = { value: [ 'create', 'get_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', ], }, }, @@ -611,7 +674,10 @@ 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', @@ -621,6 +687,86 @@ 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: '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', @@ -650,14 +796,20 @@ 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', @@ -813,6 +965,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', ], }, }, @@ -836,6 +992,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', ], }, }, @@ -921,7 +1081,27 @@ 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', ], config: { tool: (params) => { @@ -965,6 +1145,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 @@ -997,8 +1181,37 @@ 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' default: return 'confluence_retrieve' } @@ -1013,6 +1226,7 @@ export const ConfluenceV2Block: BlockConfig = { attachmentComment, blogPostId, versionNumber, + accountId, propertyKey, propertyValue, propertyId, @@ -1022,6 +1236,15 @@ export const ConfluenceV2Block: BlockConfig = { purge, bodyFormat, cursor, + taskId, + taskStatus, + taskAssignedTo, + spaceName, + spaceKey, + spaceDescription, + spacePropertyKey, + spacePropertyValue, + spacePropertyId, ...rest } = params @@ -1069,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', @@ -1081,6 +1304,9 @@ export const ConfluenceV2Block: BlockConfig = { 'list_page_versions', 'list_page_properties', 'list_labels', + 'get_page_descendants', + 'list_space_permissions', + 'list_space_properties', ] if (supportsCursor.includes(operation) && cursor) { @@ -1152,6 +1378,122 @@ export const ConfluenceV2Block: BlockConfig = { } } + if (operation === 'get_user') { + return { + credential: oauthCredential, + operation, + accountId: accountId ? String(accountId).trim() : undefined, + ...rest, + } + } + + 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, @@ -1171,6 +1513,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' }, @@ -1192,6 +1535,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' }, @@ -1242,6 +1594,23 @@ 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' }, + // 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/blocks/blocks/google_translate.ts b/apps/sim/blocks/blocks/google_translate.ts index 81815f890f..19c3236fb6 100644 --- a/apps/sim/blocks/blocks/google_translate.ts +++ b/apps/sim/blocks/blocks/google_translate.ts @@ -135,7 +135,7 @@ const SUPPORTED_LANGUAGES = [ { label: 'Yiddish', id: 'yi' }, { label: 'Yoruba', id: 'yo' }, { label: 'Zulu', id: 'zu' }, -] as const +] satisfies { label: string; id: string }[] export const GoogleTranslateBlock: BlockConfig = { type: 'google_translate', 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, 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/socket/database/operations.ts b/apps/sim/socket/database/operations.ts index 26ed43a325..7fef6aab61 100644 --- a/apps/sim/socket/database/operations.ts +++ b/apps/sim/socket/database/operations.ts @@ -270,9 +270,7 @@ async function auditWorkflowLockToggle(workflowId: string, actorId: string): Pro resourceType: AuditResourceType.WORKFLOW, resourceId: workflowId, resourceName: wf.name, - description: allLocked - ? `Locked workflow "${wf.name}"` - : `Unlocked workflow "${wf.name}"`, + description: allLocked ? `Locked workflow "${wf.name}"` : `Unlocked workflow "${wf.name}"`, metadata: { blockCount: blocks.length }, }) } 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..a9e0bc5a32 --- /dev/null +++ b/apps/sim/tools/confluence/get_page_descendants.ts @@ -0,0 +1,147 @@ +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 + type: string | null + status: string | null + spaceId: string | null + parentId: string | null + childPosition: number | null + depth: 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' }, + 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 }, + }, + }, + }, + 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..cf0b617765 --- /dev/null +++ b/apps/sim/tools/confluence/get_task.ts @@ -0,0 +1,130 @@ +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 + body: string | null + 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 = + { + 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 ?? '', + body: task.body ?? null, + 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)' }, + 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 }, + 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 new file mode 100644 index 0000000000..2304836135 --- /dev/null +++ b/apps/sim/tools/confluence/get_user.ts @@ -0,0 +1,113 @@ +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 = + { + id: 'confluence_get_user', + name: 'Confluence Get User', + description: 'Get display name and profile info for a Confluence user by 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: () => '/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) => { + 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..0dbcf4024c 100644 --- a/apps/sim/tools/confluence/index.ts +++ b/apps/sim/tools/confluence/index.ts @@ -3,17 +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 { 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' @@ -23,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' @@ -64,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 { @@ -76,6 +90,7 @@ export { confluenceListPagesInSpaceTool, confluenceGetPageChildrenTool, confluenceGetPageAncestorsTool, + confluenceGetPageDescendantsTool, // Page Version Tools confluenceListPageVersionsTool, confluenceGetPageVersionTool, @@ -87,6 +102,8 @@ export { confluenceListBlogPostsTool, confluenceGetBlogPostTool, confluenceCreateBlogPostTool, + confluenceUpdateBlogPostTool, + confluenceDeleteBlogPostTool, confluenceListBlogPostsInSpaceTool, // Search Tools confluenceSearchTool, @@ -106,9 +123,24 @@ export { confluenceDeleteLabelTool, confluenceGetPagesByLabelTool, confluenceListSpaceLabelsTool, + // User Tools + 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..3d8fe00f2b --- /dev/null +++ b/apps/sim/tools/confluence/list_space_permissions.ts @@ -0,0 +1,156 @@ +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..4f44678a89 --- /dev/null +++ b/apps/sim/tools/confluence/list_tasks.ts @@ -0,0 +1,181 @@ +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 + body: string | null + 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)' }, + 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 }, + 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..d7d87387eb --- /dev/null +++ b/apps/sim/tools/confluence/update_task.ts @@ -0,0 +1,141 @@ +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 + body: string | null + 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 ?? '', + body: task.body ?? null, + 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' }, + 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 }, + 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 2d1b0e24be..64e49bfc63 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -190,17 +190,25 @@ import { confluenceCreateCommentTool, confluenceCreatePagePropertyTool, confluenceCreatePageTool, + confluenceCreateSpacePropertyTool, + confluenceCreateSpaceTool, confluenceDeleteAttachmentTool, + confluenceDeleteBlogPostTool, confluenceDeleteCommentTool, confluenceDeleteLabelTool, confluenceDeletePagePropertyTool, confluenceDeletePageTool, + confluenceDeleteSpacePropertyTool, + confluenceDeleteSpaceTool, confluenceGetBlogPostTool, confluenceGetPageAncestorsTool, confluenceGetPageChildrenTool, + confluenceGetPageDescendantsTool, confluenceGetPagesByLabelTool, confluenceGetPageVersionTool, confluenceGetSpaceTool, + confluenceGetTaskTool, + confluenceGetUserTool, confluenceListAttachmentsTool, confluenceListBlogPostsInSpaceTool, confluenceListBlogPostsTool, @@ -210,11 +218,17 @@ import { confluenceListPagesInSpaceTool, confluenceListPageVersionsTool, confluenceListSpaceLabelsTool, + confluenceListSpacePermissionsTool, + confluenceListSpacePropertiesTool, confluenceListSpacesTool, + confluenceListTasksTool, confluenceRetrieveTool, confluenceSearchInSpaceTool, confluenceSearchTool, + confluenceUpdateBlogPostTool, confluenceUpdateCommentTool, + confluenceUpdateSpaceTool, + confluenceUpdateTaskTool, confluenceUpdateTool, confluenceUploadAttachmentTool, } from '@/tools/confluence' @@ -3032,8 +3046,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, diff --git a/bun.lock b/bun.lock index d0b2fce86d..3d4fc92887 100644 --- a/bun.lock +++ b/bun.lock @@ -13,7 +13,7 @@ "glob": "13.0.0", "husky": "9.1.7", "lint-staged": "16.0.0", - "turbo": "2.8.10", + "turbo": "2.8.11", }, }, "apps/docs": { @@ -3437,19 +3437,19 @@ "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], - "turbo": ["turbo@2.8.10", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.10", "turbo-darwin-arm64": "2.8.10", "turbo-linux-64": "2.8.10", "turbo-linux-arm64": "2.8.10", "turbo-windows-64": "2.8.10", "turbo-windows-arm64": "2.8.10" }, "bin": { "turbo": "bin/turbo" } }, "sha512-OxbzDES66+x7nnKGg2MwBA1ypVsZoDTLHpeaP4giyiHSixbsiTaMyeJqbEyvBdp5Cm28fc+8GG6RdQtic0ijwQ=="], + "turbo": ["turbo@2.8.11", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.11", "turbo-darwin-arm64": "2.8.11", "turbo-linux-64": "2.8.11", "turbo-linux-arm64": "2.8.11", "turbo-windows-64": "2.8.11", "turbo-windows-arm64": "2.8.11" }, "bin": { "turbo": "bin/turbo" } }, "sha512-H+rwSHHPLoyPOSoHdmI1zY0zy0GGj1Dmr7SeJW+nZiWLz2nex8EJ+fkdVabxXFMNEux+aywI4Sae8EqhmnOv4A=="], - "turbo-darwin-64": ["turbo-darwin-64@2.8.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-A03fXh+B7S8mL3PbdhTd+0UsaGrhfyPkODvzBDpKRY7bbeac4MDFpJ7I+Slf2oSkCEeSvHKR7Z4U71uKRUfX7g=="], + "turbo-darwin-64": ["turbo-darwin-64@2.8.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-XKaCWaz4OCt77oYYvGCIRpvYD4c/aNaKjRkUpv+e8rN3RZb+5Xsyew4yRO+gaHdMIUhQznXNXfHlhs+/p7lIhA=="], - "turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-sidzowgWL3s5xCHLeqwC9M3s9M0i16W1nuQF3Mc7fPHpZ+YPohvcbVFBB2uoRRHYZg6yBnwD4gyUHKTeXfwtXA=="], + "turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VvynLHGUNvQ9k7GZjRPSsRcK4VkioTfFb7O7liAk4nHKjEcMdls7GqxzjVWgJiKz3hWmQGaP9hRa9UUnhVWCxA=="], - "turbo-linux-64": ["turbo-linux-64@2.8.10", "", { "os": "linux", "cpu": "x64" }, "sha512-YK9vcpL3TVtqonB021XwgaQhY9hJJbKKUhLv16osxV0HkcQASQWUqR56yMge7puh6nxU67rQlTq1b7ksR1T3KA=="], + "turbo-linux-64": ["turbo-linux-64@2.8.11", "", { "os": "linux", "cpu": "x64" }, "sha512-cbSn37dcm+EmkQ7DD0euy7xV7o2el4GAOr1XujvkAyKjjNvQ+6QIUeDgQcwAx3D17zPpDvfDMJY2dLQadWnkmQ=="], - "turbo-linux-arm64": ["turbo-linux-arm64@2.8.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-3+j2tL0sG95iBJTm+6J8/45JsETQABPqtFyYjVjBbi6eVGdtNTiBmHNKrbvXRlQ3ZbUG75bKLaSSDHSEEN+btQ=="], + "turbo-linux-arm64": ["turbo-linux-arm64@2.8.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-+trymp2s2aBrhS04l6qFxcExzZ8ffndevuUB9c5RCeqsVpZeiWuGQlWNm5XjOmzoMayxRARZ5ma7yiWbGMiLqQ=="], - "turbo-windows-64": ["turbo-windows-64@2.8.10", "", { "os": "win32", "cpu": "x64" }, "sha512-hdeF5qmVY/NFgiucf8FW0CWJWtyT2QPm5mIsX0W1DXAVzqKVXGq+Zf+dg4EUngAFKjDzoBeN6ec2Fhajwfztkw=="], + "turbo-windows-64": ["turbo-windows-64@2.8.11", "", { "os": "win32", "cpu": "x64" }, "sha512-3kJjFSM4yw1n9Uzmi+XkAUgCae19l/bH6RJ442xo7mnZm0tpOjo33F+FYHoSVpIWVMd0HG0LDccyafPSdylQbA=="], - "turbo-windows-arm64": ["turbo-windows-arm64@2.8.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-QGdr/Q8LWmj+ITMkSvfiz2glf0d7JG0oXVzGL3jxkGqiBI1zXFj20oqVY0qWi+112LO9SVrYdpHS0E/oGFrMbQ=="], + "turbo-windows-arm64": ["turbo-windows-arm64@2.8.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-JOM4uF2vuLsJUvibdR6X9QqdZr6BhC6Nhlrw4LKFPsXZZI/9HHLoqAiYRpE4MuzIwldCH/jVySnWXrI1SKto0g=="], "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], diff --git a/package.json b/package.json index 55e9fe8fdc..d4a932a341 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "glob": "13.0.0", "husky": "9.1.7", "lint-staged": "16.0.0", - "turbo": "2.8.10" + "turbo": "2.8.11" }, "lint-staged": { "*.{js,jsx,ts,tsx,json,css,scss}": [ 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',