Merge remote-tracking branch 'origin/staging' into fix/onedrive

This commit is contained in:
Vikhyath Mondreti
2026-02-04 22:46:23 -08:00
132 changed files with 5319 additions and 637 deletions

View File

@@ -163,9 +163,9 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
elevenlabs: ElevenLabsIcon,
enrich: EnrichSoIcon,
exa: ExaAIIcon,
file_v2: DocumentIcon,
file_v3: DocumentIcon,
firecrawl: FirecrawlIcon,
fireflies: FirefliesIcon,
fireflies_v2: FirefliesIcon,
github_v2: GithubIcon,
gitlab: GitLabIcon,
gmail_v2: GmailIcon,
@@ -177,7 +177,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
google_maps: GoogleMapsIcon,
google_search: GoogleIcon,
google_sheets_v2: GoogleSheetsIcon,
google_slides: GoogleSlidesIcon,
google_slides_v2: GoogleSlidesIcon,
google_vault: GoogleVaultIcon,
grafana: GrafanaIcon,
grain: GrainIcon,
@@ -206,7 +206,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
microsoft_excel_v2: MicrosoftExcelIcon,
microsoft_planner: MicrosoftPlannerIcon,
microsoft_teams: MicrosoftTeamsIcon,
mistral_parse_v2: MistralIcon,
mistral_parse_v3: MistralIcon,
mongodb: MongoDBIcon,
mysql: MySQLIcon,
neo4j: Neo4jIcon,
@@ -221,11 +221,11 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
polymarket: PolymarketIcon,
postgresql: PostgresIcon,
posthog: PosthogIcon,
pulse: PulseIcon,
pulse_v2: PulseIcon,
qdrant: QdrantIcon,
rds: RDSIcon,
reddit: RedditIcon,
reducto: ReductoIcon,
reducto_v2: ReductoIcon,
resend: ResendIcon,
s3: S3Icon,
salesforce: SalesforceIcon,
@@ -244,11 +244,11 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
ssh: SshIcon,
stagehand: StagehandIcon,
stripe: StripeIcon,
stt: STTIcon,
stt_v2: STTIcon,
supabase: SupabaseIcon,
tavily: TavilyIcon,
telegram: TelegramIcon,
textract: TextractIcon,
textract_v2: TextractIcon,
tinybird: TinybirdIcon,
translate: TranslateIcon,
trello: TrelloIcon,
@@ -257,7 +257,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
twilio_voice: TwilioIcon,
typeform: TypeformIcon,
video_generator_v2: VideoIcon,
vision: EyeIcon,
vision_v2: EyeIcon,
wealthbox: WealthboxIcon,
webflow: WebflowIcon,
whatsapp: WhatsAppIcon,

View File

@@ -6,7 +6,7 @@ description: Mehrere Dateien lesen und parsen
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="file"
type="file_v3"
color="#40916C"
/>

View File

@@ -6,7 +6,7 @@ description: Interagieren Sie mit Fireflies.ai-Besprechungstranskripten und -auf
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="fireflies"
type="fireflies_v2"
color="#100730"
/>

View File

@@ -6,7 +6,7 @@ description: Text aus PDF-Dokumenten extrahieren
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="mistral_parse"
type="mistral_parse_v3"
color="#000000"
/>

View File

@@ -49,10 +49,25 @@ Retrieve content from Confluence pages using the Confluence API.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of retrieval |
| `ts` | string | ISO 8601 timestamp of the operation |
| `pageId` | string | Confluence page ID |
| `content` | string | Page content with HTML tags stripped |
| `title` | string | Page title |
| `content` | string | Page content with HTML tags stripped |
| `status` | string | Page status \(current, archived, trashed, draft\) |
| `spaceId` | string | ID of the space containing the page |
| `parentId` | string | ID of the parent page |
| `authorId` | string | Account ID of the page author |
| `createdAt` | string | ISO 8601 timestamp when the page was created |
| `url` | string | URL to view the page in Confluence |
| `body` | object | Raw page body content in storage format |
| ↳ `value` | string | The content value in the specified format |
| ↳ `representation` | string | Content representation type |
| `version` | object | Page version information |
| ↳ `number` | number | Version number |
| ↳ `message` | string | Version message |
| ↳ `minorEdit` | boolean | Whether this is a minor edit |
| ↳ `authorId` | string | Account ID of the version author |
| ↳ `createdAt` | string | ISO 8601 timestamp of version creation |
### `confluence_update`
@@ -76,6 +91,25 @@ Update a Confluence page using the Confluence API.
| `ts` | string | Timestamp of update |
| `pageId` | string | Confluence page ID |
| `title` | string | Updated page title |
| `status` | string | Page status |
| `spaceId` | string | Space ID |
| `body` | object | Page body content in storage format |
| ↳ `storage` | object | Body in storage format \(Confluence markup\) |
| ↳ `value` | string | The content value in the specified format |
| ↳ `representation` | string | Content representation type |
| ↳ `view` | object | Body in view format \(rendered HTML\) |
| ↳ `value` | string | The content value in the specified format |
| ↳ `representation` | string | Content representation type |
| ↳ `atlas_doc_format` | object | Body in Atlassian Document Format \(ADF\) |
| ↳ `value` | string | The content value in the specified format |
| ↳ `representation` | string | Content representation type |
| `version` | object | Page version information |
| ↳ `number` | number | Version number |
| ↳ `message` | string | Version message |
| ↳ `minorEdit` | boolean | Whether this is a minor edit |
| ↳ `authorId` | string | Account ID of the version author |
| ↳ `createdAt` | string | ISO 8601 timestamp of version creation |
| `url` | string | URL to view the page in Confluence |
| `success` | boolean | Update operation success status |
### `confluence_create_page`
@@ -100,11 +134,30 @@ Create a new page in a Confluence space.
| `ts` | string | Timestamp of creation |
| `pageId` | string | Created page ID |
| `title` | string | Page title |
| `status` | string | Page status |
| `spaceId` | string | Space ID |
| `parentId` | string | Parent page ID |
| `body` | object | Page body content |
| ↳ `storage` | object | Body in storage format \(Confluence markup\) |
| ↳ `value` | string | The content value in the specified format |
| ↳ `representation` | string | Content representation type |
| ↳ `view` | object | Body in view format \(rendered HTML\) |
| ↳ `value` | string | The content value in the specified format |
| ↳ `representation` | string | Content representation type |
| ↳ `atlas_doc_format` | object | Body in Atlassian Document Format \(ADF\) |
| ↳ `value` | string | The content value in the specified format |
| ↳ `representation` | string | Content representation type |
| `version` | object | Page version information |
| ↳ `number` | number | Version number |
| ↳ `message` | string | Version message |
| ↳ `minorEdit` | boolean | Whether this is a minor edit |
| ↳ `authorId` | string | Account ID of the version author |
| ↳ `createdAt` | string | ISO 8601 timestamp of version creation |
| `url` | string | Page URL |
### `confluence_delete_page`
Delete a Confluence page (moves it to trash where it can be restored).
Delete a Confluence page. By default moves to trash; use purge=true to permanently delete.
#### Input
@@ -112,6 +165,7 @@ Delete a Confluence page (moves it to trash where it can be restored).
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `pageId` | string | Yes | Confluence page ID to delete |
| `purge` | boolean | No | If true, permanently deletes the page instead of moving to trash \(default: false\) |
| `cloudId` | string | No | Confluence Cloud ID for the instance. If not provided, it will be fetched using the domain. |
#### Output
@@ -122,6 +176,229 @@ Delete a Confluence page (moves it to trash where it can be restored).
| `pageId` | string | Deleted page ID |
| `deleted` | boolean | Deletion status |
### `confluence_list_pages_in_space`
List all pages within a specific Confluence space. Supports pagination and filtering by status.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `spaceId` | string | Yes | The ID of the Confluence space to list pages from |
| `limit` | number | No | Maximum number of pages to return \(default: 50, max: 250\) |
| `status` | string | No | Filter pages by status: current, archived, trashed, or draft |
| `bodyFormat` | string | No | Format for page body content: storage, atlas_doc_format, or view. If not specified, body is not included. |
| `cursor` | string | No | Pagination cursor from previous response to get the next page of results |
| `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 |
| `pages` | array | Array of pages in the space |
| ↳ `id` | string | Unique page identifier |
| ↳ `title` | string | Page title |
| ↳ `status` | string | Page status \(e.g., current, archived, trashed, draft\) |
| ↳ `spaceId` | string | ID of the space containing the page |
| ↳ `parentId` | string | ID of the parent page \(null if top-level\) |
| ↳ `authorId` | string | Account ID of the page author |
| ↳ `createdAt` | string | ISO 8601 timestamp when the page was created |
| ↳ `version` | object | Page version information |
| ↳ `number` | number | Version number |
| ↳ `message` | string | Version message |
| ↳ `minorEdit` | boolean | Whether this is a minor edit |
| ↳ `authorId` | string | Account ID of the version author |
| ↳ `createdAt` | string | ISO 8601 timestamp of version creation |
| ↳ `body` | object | Page body content \(if bodyFormat was specified\) |
| ↳ `storage` | object | Body in storage format \(Confluence markup\) |
| ↳ `value` | string | The content value in the specified format |
| ↳ `representation` | string | Content representation type |
| ↳ `view` | object | Body in view format \(rendered HTML\) |
| ↳ `value` | string | The content value in the specified format |
| ↳ `representation` | string | Content representation type |
| ↳ `atlas_doc_format` | object | Body in Atlassian Document Format \(ADF\) |
| ↳ `value` | string | The content value in the specified format |
| ↳ `representation` | string | Content representation type |
| ↳ `webUrl` | string | URL to view the page in Confluence |
| `nextCursor` | string | Cursor for fetching the next page of results |
### `confluence_get_page_children`
Get all child pages of a specific Confluence page. Useful for navigating page hierarchies.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `pageId` | string | Yes | The ID of the parent page to get children from |
| `limit` | number | No | Maximum number of child pages to return \(default: 50, max: 250\) |
| `cursor` | string | No | Pagination cursor from previous response to get the next page of results |
| `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 |
| `parentId` | string | ID of the parent page |
| `children` | array | Array of child pages |
| ↳ `id` | string | Child page ID |
| ↳ `title` | string | Child page title |
| ↳ `status` | string | Page status |
| ↳ `spaceId` | string | Space ID |
| ↳ `childPosition` | number | Position among siblings |
| ↳ `webUrl` | string | URL to view the page |
| `nextCursor` | string | Cursor for fetching the next page of results |
### `confluence_get_page_ancestors`
Get the ancestor (parent) pages of a specific Confluence page. Returns the full hierarchy from the page up to the root.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `pageId` | string | Yes | The ID of the page to get ancestors for |
| `limit` | number | No | Maximum number of ancestors to return \(default: 25, max: 250\) |
| `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 |
| `pageId` | string | ID of the page whose ancestors were retrieved |
| `ancestors` | array | Array of ancestor pages, ordered from direct parent to root |
| ↳ `id` | string | Ancestor page ID |
| ↳ `title` | string | Ancestor page title |
| ↳ `status` | string | Page status |
| ↳ `spaceId` | string | Space ID |
| ↳ `webUrl` | string | URL to view the page |
### `confluence_list_page_versions`
List all versions (revision history) of a Confluence page.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `pageId` | string | Yes | The ID of the page to get versions for |
| `limit` | number | No | Maximum number of versions 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 |
| `pageId` | string | ID of the page |
| `versions` | array | Array of page versions |
| ↳ `number` | number | Version number |
| ↳ `message` | string | Version message |
| ↳ `minorEdit` | boolean | Whether this is a minor edit |
| ↳ `authorId` | string | Account ID of the version author |
| ↳ `createdAt` | string | ISO 8601 timestamp of version creation |
| `nextCursor` | string | Cursor for fetching the next page of results |
### `confluence_get_page_version`
Get details about a specific version of a Confluence page.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `pageId` | string | Yes | The ID of the page |
| `versionNumber` | number | Yes | The version number to retrieve \(e.g., 1, 2, 3\) |
| `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 |
| `pageId` | string | ID of the page |
| `version` | object | Detailed version information |
| ↳ `number` | number | Version number |
| ↳ `message` | string | Version message |
| ↳ `minorEdit` | boolean | Whether this is a minor edit |
| ↳ `authorId` | string | Account ID of the version author |
| ↳ `createdAt` | string | ISO 8601 timestamp of version creation |
| ↳ `contentTypeModified` | boolean | Whether the content type was modified in this version |
| ↳ `collaborators` | array | List of collaborator account IDs for this version |
| ↳ `prevVersion` | number | Previous version number |
| ↳ `nextVersion` | number | Next version number |
### `confluence_list_page_properties`
List all custom properties (metadata) attached to a Confluence page.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `pageId` | string | Yes | The ID of the page to list properties from |
| `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 |
| `pageId` | string | ID of the page |
| `properties` | array | Array of content properties |
| ↳ `id` | string | Property ID |
| ↳ `key` | string | Property key |
| ↳ `value` | json | Property value \(can be any JSON\) |
| ↳ `version` | object | Version information |
| ↳ `number` | number | Version number |
| ↳ `message` | string | Version message |
| ↳ `minorEdit` | boolean | Whether this is a minor edit |
| ↳ `authorId` | string | Account ID of the version author |
| ↳ `createdAt` | string | ISO 8601 timestamp of version creation |
| `nextCursor` | string | Cursor for fetching the next page of results |
### `confluence_create_page_property`
Create a new custom property (metadata) on a Confluence page.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `pageId` | string | Yes | The ID of the page to add the property to |
| `key` | string | Yes | The key/name for the property |
| `value` | json | Yes | The value for the property \(can be any JSON value\) |
| `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 |
| `pageId` | string | ID of the page |
| `propertyId` | string | ID of the created property |
| `key` | string | Property key |
| `value` | json | Property value |
| `version` | object | Version information |
| ↳ `number` | number | Version number |
| ↳ `message` | string | Version message |
| ↳ `minorEdit` | boolean | Whether this is a minor edit |
| ↳ `authorId` | string | Account ID of the version author |
| ↳ `createdAt` | string | ISO 8601 timestamp of version creation |
### `confluence_search`
Search for content across Confluence pages, blog posts, and other content.
@@ -155,6 +432,211 @@ Search for content across Confluence pages, blog posts, and other content.
| ↳ `lastModified` | string | ISO 8601 timestamp of last modification |
| ↳ `entityType` | string | Entity type identifier \(e.g., content, space\) |
### `confluence_search_in_space`
Search for content within a specific Confluence space. Optionally filter by text query and content type.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `spaceKey` | string | Yes | The key of the Confluence space to search in \(e.g., "ENG", "HR"\) |
| `query` | string | No | Text search query. If not provided, returns all content in the space. |
| `contentType` | string | No | Filter by content type: page, blogpost, attachment, or comment |
| `limit` | number | No | Maximum number of results to return \(default: 25, max: 250\) |
| `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 |
| `spaceKey` | string | The space key that was searched |
| `totalSize` | number | Total number of matching results |
| `results` | array | Array of search results |
| ↳ `id` | string | Unique content identifier |
| ↳ `title` | string | Content title |
| ↳ `type` | string | Content type \(e.g., page, blogpost, attachment, comment\) |
| ↳ `status` | string | Content status \(e.g., current\) |
| ↳ `url` | string | URL to view the content in Confluence |
| ↳ `excerpt` | string | Text excerpt matching the search query |
| ↳ `spaceKey` | string | Key of the space containing the content |
| ↳ `space` | object | Space information for the content |
| ↳ `id` | string | Space identifier |
| ↳ `key` | string | Space key |
| ↳ `name` | string | Space name |
| ↳ `lastModified` | string | ISO 8601 timestamp of last modification |
| ↳ `entityType` | string | Entity type identifier \(e.g., content, space\) |
### `confluence_list_blogposts`
List all blog posts across all accessible Confluence spaces.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `limit` | number | No | Maximum number of blog posts to return \(default: 25, max: 250\) |
| `status` | string | No | Filter by status: current, archived, trashed, or draft |
| `sort` | string | No | Sort order: created-date, -created-date, modified-date, -modified-date, title, -title |
| `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 |
| `blogPosts` | array | Array of blog posts |
| ↳ `id` | string | Blog post ID |
| ↳ `title` | string | Blog post title |
| ↳ `status` | string | Blog post status |
| ↳ `spaceId` | string | Space ID |
| ↳ `authorId` | string | Author account ID |
| ↳ `createdAt` | string | Creation timestamp |
| ↳ `version` | object | Version information |
| ↳ `number` | number | Version number |
| ↳ `message` | string | Version message |
| ↳ `minorEdit` | boolean | Whether this is a minor edit |
| ↳ `authorId` | string | Account ID of the version author |
| ↳ `createdAt` | string | ISO 8601 timestamp of version creation |
| ↳ `webUrl` | string | URL to view the blog post |
| `nextCursor` | string | Cursor for fetching the next page of results |
### `confluence_get_blogpost`
Get a specific Confluence blog post by ID, including its 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 retrieve |
| `bodyFormat` | string | No | Format for blog post body: storage, atlas_doc_format, or view |
| `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 | Blog post ID |
| `title` | string | Blog post title |
| `status` | string | Blog post status |
| `spaceId` | string | Space ID |
| `authorId` | string | Author account ID |
| `createdAt` | string | Creation timestamp |
| `version` | object | Version information |
| ↳ `number` | number | Version number |
| ↳ `message` | string | Version message |
| ↳ `minorEdit` | boolean | Whether this is a minor edit |
| ↳ `authorId` | string | Account ID of the version author |
| ↳ `createdAt` | string | ISO 8601 timestamp of version creation |
| `body` | object | Blog post body content in requested format\(s\) |
| ↳ `storage` | object | Body in storage format \(Confluence markup\) |
| ↳ `value` | string | The content value in the specified format |
| ↳ `representation` | string | Content representation type |
| ↳ `view` | object | Body in view format \(rendered HTML\) |
| ↳ `value` | string | The content value in the specified format |
| ↳ `representation` | string | Content representation type |
| ↳ `atlas_doc_format` | object | Body in Atlassian Document Format \(ADF\) |
| ↳ `value` | string | The content value in the specified format |
| ↳ `representation` | string | Content representation type |
| `webUrl` | string | URL to view the blog post |
### `confluence_create_blogpost`
Create a new blog post in a Confluence space.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `spaceId` | string | Yes | The ID of the space to create the blog post in |
| `title` | string | Yes | Title of the blog post |
| `content` | string | Yes | Blog post content in Confluence storage format \(HTML\) |
| `status` | string | No | Blog post status: current \(default\) or draft |
| `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 | Created blog post ID |
| `title` | string | Blog post title |
| `status` | string | Blog post status |
| `spaceId` | string | Space ID |
| `authorId` | string | Author account ID |
| `body` | object | Blog post body content |
| ↳ `storage` | object | Body in storage format \(Confluence markup\) |
| ↳ `value` | string | The content value in the specified format |
| ↳ `representation` | string | Content representation type |
| ↳ `view` | object | Body in view format \(rendered HTML\) |
| ↳ `value` | string | The content value in the specified format |
| ↳ `representation` | string | Content representation type |
| ↳ `atlas_doc_format` | object | Body in Atlassian Document Format \(ADF\) |
| ↳ `value` | string | The content value in the specified format |
| ↳ `representation` | string | Content representation type |
| `version` | object | Blog post version information |
| ↳ `number` | number | Version number |
| ↳ `message` | string | Version message |
| ↳ `minorEdit` | boolean | Whether this is a minor edit |
| ↳ `authorId` | string | Account ID of the version author |
| ↳ `createdAt` | string | ISO 8601 timestamp of version creation |
| `webUrl` | string | URL to view the blog post |
### `confluence_list_blogposts_in_space`
List all blog posts within a specific Confluence space.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `spaceId` | string | Yes | The ID of the Confluence space to list blog posts from |
| `limit` | number | No | Maximum number of blog posts to return \(default: 25, max: 250\) |
| `status` | string | No | Filter by status: current, archived, trashed, or draft |
| `bodyFormat` | string | No | Format for blog post body: storage, atlas_doc_format, or view |
| `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 |
| `blogPosts` | array | Array of blog posts in the space |
| ↳ `id` | string | Blog post ID |
| ↳ `title` | string | Blog post title |
| ↳ `status` | string | Blog post status |
| ↳ `spaceId` | string | Space ID |
| ↳ `authorId` | string | Author account ID |
| ↳ `createdAt` | string | Creation timestamp |
| ↳ `version` | object | Version information |
| ↳ `number` | number | Version number |
| ↳ `message` | string | Version message |
| ↳ `minorEdit` | boolean | Whether this is a minor edit |
| ↳ `authorId` | string | Account ID of the version author |
| ↳ `createdAt` | string | ISO 8601 timestamp of version creation |
| ↳ `body` | object | Blog post body content |
| ↳ `storage` | object | Body in storage format \(Confluence markup\) |
| ↳ `value` | string | The content value in the specified format |
| ↳ `representation` | string | Content representation type |
| ↳ `view` | object | Body in view format \(rendered HTML\) |
| ↳ `value` | string | The content value in the specified format |
| ↳ `representation` | string | Content representation type |
| ↳ `atlas_doc_format` | object | Body in Atlassian Document Format \(ADF\) |
| ↳ `value` | string | The content value in the specified format |
| ↳ `representation` | string | Content representation type |
| ↳ `webUrl` | string | URL to view the blog post |
| `nextCursor` | string | Cursor for fetching the next page of results |
### `confluence_create_comment`
Add a comment to a Confluence page.
@@ -187,6 +669,8 @@ List all comments on a Confluence page.
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `pageId` | string | Yes | Confluence page ID to list comments from |
| `limit` | number | No | Maximum number of comments to return \(default: 25\) |
| `bodyFormat` | string | No | Format for the comment body: storage, atlas_doc_format, view, or export_view \(default: storage\) |
| `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
@@ -212,6 +696,7 @@ List all comments on a Confluence page.
| ↳ `minorEdit` | boolean | Whether this is a minor edit |
| ↳ `authorId` | string | Account ID of the version author |
| ↳ `createdAt` | string | ISO 8601 timestamp of version creation |
| `nextCursor` | string | Cursor for fetching the next page of results |
### `confluence_update_comment`
@@ -291,7 +776,8 @@ List all attachments on a Confluence page.
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `pageId` | string | Yes | Confluence page ID to list attachments from |
| `limit` | number | No | Maximum number of attachments to return \(default: 25\) |
| `limit` | number | No | Maximum number of attachments 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
@@ -316,6 +802,7 @@ List all attachments on a Confluence page.
| ↳ `minorEdit` | boolean | Whether this is a minor edit |
| ↳ `authorId` | string | Account ID of the version author |
| ↳ `createdAt` | string | ISO 8601 timestamp of version creation |
| `nextCursor` | string | Cursor for fetching the next page of results |
### `confluence_delete_attachment`
@@ -347,6 +834,8 @@ List all labels on a Confluence page.
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `pageId` | string | Yes | Confluence page ID to list labels from |
| `limit` | number | No | Maximum number of labels to return \(default: 25, 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
@@ -358,6 +847,30 @@ List all labels on a Confluence page.
| ↳ `id` | string | Unique label identifier |
| ↳ `name` | string | Label name |
| ↳ `prefix` | string | Label prefix/type \(e.g., global, my, team\) |
| `nextCursor` | string | Cursor for fetching the next page of results |
### `confluence_add_label`
Add a label to a Confluence page for organization and categorization.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `pageId` | string | Yes | Confluence page ID to add the label to |
| `labelName` | string | Yes | Name of the label to add |
| `prefix` | string | No | Label prefix: global \(default\), my, team, or system |
| `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 |
| `pageId` | string | Page ID that the label was added to |
| `labelName` | string | Name of the added label |
| `labelId` | string | ID of the added label |
### `confluence_get_space`
@@ -375,13 +888,19 @@ Get details about a specific Confluence space.
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of retrieval |
| `ts` | string | ISO 8601 timestamp of the operation |
| `spaceId` | string | Space ID |
| `name` | string | Space name |
| `key` | string | Space key |
| `type` | string | Space type |
| `status` | string | Space status |
| `url` | string | Space URL |
| `type` | string | Space type \(global, personal\) |
| `status` | string | Space status \(current, archived\) |
| `url` | string | URL to view the space in Confluence |
| `authorId` | string | Account ID of the space creator |
| `createdAt` | string | ISO 8601 timestamp when the space was created |
| `homepageId` | string | ID of the space homepage |
| `description` | object | Space description content |
| ↳ `value` | string | Description text content |
| ↳ `representation` | string | Content representation format \(e.g., plain, view, storage\) |
### `confluence_list_spaces`
@@ -392,7 +911,8 @@ List all Confluence spaces accessible to the user.
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Confluence domain \(e.g., yourcompany.atlassian.net\) |
| `limit` | number | No | Maximum number of spaces to return \(default: 25\) |
| `limit` | number | No | Maximum number of spaces to return \(default: 25, 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
@@ -412,5 +932,6 @@ List all Confluence spaces accessible to the user.
| ↳ `description` | object | Space description |
| ↳ `value` | string | Description text content |
| ↳ `representation` | string | Content representation format \(e.g., plain, view, storage\) |
| `nextCursor` | string | Cursor for fetching the next page of results |

View File

@@ -63,6 +63,7 @@ Send a message to a Discord channel
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Success or error message |
| `files` | file[] | Files attached to the message |
| `data` | object | Discord message data |
| ↳ `id` | string | Message ID |
| ↳ `content` | string | Message content |

View File

@@ -43,7 +43,8 @@ Upload a file to Dropbox
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `path` | string | Yes | The path in Dropbox where the file should be saved \(e.g., /folder/document.pdf\) |
| `fileContent` | string | Yes | The base64 encoded content of the file to upload |
| `file` | file | No | The file to upload \(UserFile object\) |
| `fileContent` | string | No | Legacy: base64 encoded file content |
| `fileName` | string | No | Optional filename \(used if path is a folder\) |
| `mode` | string | No | Write mode: add \(default\) or overwrite |
| `autorename` | boolean | No | If true, rename the file if there is a conflict |
@@ -66,7 +67,7 @@ Upload a file to Dropbox
### `dropbox_download`
Download a file from Dropbox and get a temporary link
Download a file from Dropbox with metadata and content
#### Input
@@ -78,11 +79,8 @@ Download a file from Dropbox and get a temporary link
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `file` | object | The file metadata |
| ↳ `id` | string | Unique identifier for the file |
| ↳ `name` | string | Name of the file |
| ↳ `path_display` | string | Display path of the file |
| ↳ `size` | number | Size of the file in bytes |
| `file` | file | Downloaded file stored in execution files |
| `metadata` | json | The file metadata |
| `temporaryLink` | string | Temporary link to download the file \(valid for ~4 hours\) |
| `content` | string | Base64 encoded file content \(if fetched\) |

View File

@@ -6,7 +6,7 @@ description: Read and parse multiple files
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="file_v2"
type="file_v3"
color="#40916C"
/>
@@ -27,7 +27,7 @@ The File Parser tool is particularly useful for scenarios where your agents need
## Usage Instructions
Integrate File into the workflow. Can upload a file manually or insert a file url.
Upload files directly or import from external URLs to get UserFile objects for use in other blocks.
@@ -41,14 +41,15 @@ Parse one or more uploaded files or files from URLs (text, PDF, CSV, images, etc
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `filePath` | string | Yes | Path to the file\(s\). Can be a single path, URL, or an array of paths. |
| `filePath` | string | No | Path to the file\(s\). Can be a single path, URL, or an array of paths. |
| `file` | file | No | Uploaded file\(s\) to parse |
| `fileType` | string | No | Type of file to parse \(auto-detected if not specified\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `files` | array | Array of parsed files with content, metadata, and file properties |
| `combinedContent` | string | All file contents merged into a single text string |
| `files` | file[] | Parsed files as UserFile objects |
| `combinedContent` | string | Combined content of all parsed files |

View File

@@ -6,7 +6,7 @@ description: Interact with Fireflies.ai meeting transcripts and recordings
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="fireflies"
type="fireflies_v2"
color="#100730"
/>

View File

@@ -692,6 +692,7 @@ Get the content of a file from a GitHub repository. Supports files up to 1MB. Co
| `download_url` | string | Direct download URL |
| `git_url` | string | Git blob API URL |
| `_links` | json | Related links |
| `file` | file | Downloaded file stored in execution files |
### `github_create_file`

View File

@@ -291,11 +291,7 @@ Download a file from Google Drive with complete metadata (exports Google Workspa
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `file` | object | Downloaded file data |
| ↳ `name` | string | File name |
| ↳ `mimeType` | string | MIME type of the file |
| ↳ `data` | string | File content as base64-encoded string |
| ↳ `size` | number | File size in bytes |
| `file` | file | Downloaded file stored in execution files |
| `metadata` | object | Complete file metadata from Google Drive |
| ↳ `id` | string | Google Drive file ID |
| ↳ `kind` | string | Resource type identifier |

View File

@@ -6,7 +6,7 @@ description: Read, write, and create presentations
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="google_slides"
type="google_slides_v2"
color="#E0E0E0"
/>

View File

@@ -333,6 +333,28 @@ Get all attachments from a Jira issue
| `issueKey` | string | Issue key |
| `attachments` | array | Array of attachments with id, filename, size, mimeType, created, author |
### `jira_add_attachment`
Add attachments to a Jira issue
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `issueKey` | string | Yes | Jira issue key to add attachments to \(e.g., PROJ-123\) |
| `files` | file[] | Yes | Files to attach to the Jira issue |
| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `issueKey` | string | Issue key |
| `attachmentIds` | json | IDs of uploaded attachments |
| `files` | file[] | Uploaded attachment files |
### `jira_delete_attachment`
Delete an attachment from a Jira issue

View File

@@ -1022,7 +1022,8 @@ Add an attachment to an issue in Linear
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `issueId` | string | Yes | Issue ID to attach to |
| `url` | string | Yes | URL of the attachment |
| `url` | string | No | URL of the attachment |
| `file` | file | No | File to attach |
| `title` | string | Yes | Attachment title |
| `subtitle` | string | No | Attachment subtitle/description |

View File

@@ -81,6 +81,7 @@ Write or update content in a Microsoft Teams chat
| `createdTime` | string | Timestamp when message was created |
| `url` | string | Web URL to the message |
| `updatedContent` | boolean | Whether content was successfully updated |
| `files` | file[] | Files attached to the message |
### `microsoft_teams_read_channel`
@@ -132,6 +133,7 @@ Write or send a message to a Microsoft Teams channel
| `createdTime` | string | Timestamp when message was created |
| `url` | string | Web URL to the message |
| `updatedContent` | boolean | Whether content was successfully updated |
| `files` | file[] | Files attached to the message |
### `microsoft_teams_update_chat_message`

View File

@@ -6,7 +6,7 @@ description: Extract text from PDF documents
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="mistral_parse_v2"
type="mistral_parse_v3"
color="#000000"
/>
@@ -35,13 +35,12 @@ Integrate Mistral Parse into the workflow. Can extract text from uploaded PDF do
### `mistral_parser`
Parse PDF documents using Mistral OCR API
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `filePath` | string | Yes | URL to a PDF document to be processed |
| `filePath` | string | No | URL to a PDF document to be processed |
| `file` | file | No | Document file to be processed |
| `fileUpload` | object | No | File upload data from file-upload component |
| `resultType` | string | No | Type of parsed result \(markdown, text, or json\). Defaults to markdown. |
| `includeImageBase64` | boolean | No | Include base64-encoded images in the response |
@@ -55,27 +54,8 @@ Parse PDF documents using Mistral OCR API
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `pages` | array | Array of page objects from Mistral OCR |
| ↳ `index` | number | Page index \(zero-based\) |
| ↳ `markdown` | string | Extracted markdown content |
| ↳ `images` | array | Images extracted from this page with bounding boxes |
| ↳ `id` | string | Image identifier \(e.g., img-0.jpeg\) |
| ↳ `top_left_x` | number | Top-left X coordinate in pixels |
| ↳ `top_left_y` | number | Top-left Y coordinate in pixels |
| ↳ `bottom_right_x` | number | Bottom-right X coordinate in pixels |
| ↳ `bottom_right_y` | number | Bottom-right Y coordinate in pixels |
| ↳ `image_base64` | string | Base64-encoded image data \(when include_image_base64=true\) |
| ↳ `dimensions` | object | Page dimensions |
| ↳ `dpi` | number | Dots per inch |
| ↳ `height` | number | Page height in pixels |
| ↳ `width` | number | Page width in pixels |
| ↳ `tables` | array | Extracted tables as HTML/markdown \(when table_format is set\). Referenced via placeholders like \[tbl-0.html\] |
| ↳ `hyperlinks` | array | Array of URL strings detected in the page \(e.g., \["https://...", "mailto:..."\]\) |
| ↳ `header` | string | Page header content \(when extract_header=true\) |
| ↳ `footer` | string | Page footer content \(when extract_footer=true\) |
| `model` | string | Mistral OCR model identifier \(e.g., mistral-ocr-latest\) |
| `usage_info` | object | Usage and processing statistics |
| ↳ `pages_processed` | number | Total number of pages processed |
| ↳ `doc_size_bytes` | number | Document file size in bytes |
| `document_annotation` | string | Structured annotation data as JSON string \(when applicable\) |
| `model` | string | Mistral OCR model identifier |
| `usage_info` | json | Usage statistics from the API |
| `document_annotation` | string | Structured annotation data |

View File

@@ -113,6 +113,26 @@ Create a new page in Notion
| `last_edited_time` | string | ISO 8601 last edit timestamp |
| `title` | string | Page title |
### `notion_update_page`
Update properties of a Notion page
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `pageId` | string | Yes | The UUID of the Notion page to update |
| `properties` | json | Yes | JSON object of properties to update |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Page UUID |
| `url` | string | Notion page URL |
| `last_edited_time` | string | ISO 8601 last edit timestamp |
| `title` | string | Page title |
### `notion_query_database`
Query and filter Notion database entries with advanced filtering

View File

@@ -152,6 +152,7 @@ Retrieve files from Pipedrive with optional filters
| `person_id` | string | No | Filter files by person ID \(e.g., "456"\) |
| `org_id` | string | No | Filter files by organization ID \(e.g., "789"\) |
| `limit` | string | No | Number of results to return \(e.g., "50", default: 100, max: 500\) |
| `downloadFiles` | boolean | No | Download file contents into file outputs |
#### Output
@@ -168,6 +169,7 @@ Retrieve files from Pipedrive with optional filters
| ↳ `person_id` | number | Associated person ID |
| ↳ `org_id` | number | Associated organization ID |
| ↳ `url` | string | File download URL |
| `downloadedFiles` | file[] | Downloaded files from Pipedrive |
| `total_items` | number | Total number of files returned |
| `success` | boolean | Operation success status |

View File

@@ -6,7 +6,7 @@ description: Extract text from documents using Pulse OCR
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="pulse"
type="pulse_v2"
color="#E0E0E0"
/>
@@ -31,7 +31,7 @@ If you need accurate, scalable, and developer-friendly document parsing capabili
## Usage Instructions
Integrate Pulse into the workflow. Extract text from PDF documents, images, and Office files via URL or upload.
Integrate Pulse into the workflow. Extract text from PDF documents, images, and Office files via upload or file references.
@@ -39,13 +39,12 @@ Integrate Pulse into the workflow. Extract text from PDF documents, images, and
### `pulse_parser`
Parse documents (PDF, images, Office docs) using Pulse OCR API
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `filePath` | string | Yes | URL to a document to be processed |
| `filePath` | string | No | URL to a document to be processed |
| `file` | file | No | Document file to be processed |
| `fileUpload` | object | No | File upload data from file-upload component |
| `pages` | string | No | Page range to process \(1-indexed, e.g., "1-2,5"\) |
| `extractFigure` | boolean | No | Enable figure extraction from the document |
@@ -57,16 +56,6 @@ Parse documents (PDF, images, Office docs) using Pulse OCR API
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `markdown` | string | Extracted content in markdown format |
| `page_count` | number | Number of pages in the document |
| `job_id` | string | Unique job identifier |
| `bounding_boxes` | json | Bounding box layout information |
| `extraction_url` | string | URL for extraction results \(for large documents\) |
| `html` | string | HTML content if requested |
| `structured_output` | json | Structured output if schema was provided |
| `chunks` | json | Chunked content if chunking was enabled |
| `figures` | json | Extracted figures if figure extraction was enabled |
This tool does not produce any outputs.

View File

@@ -6,7 +6,7 @@ description: Extract text from PDF documents
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="reducto"
type="reducto_v2"
color="#5c0c5c"
/>
@@ -29,7 +29,7 @@ Looking for reliable and scalable PDF parsing? Reducto is optimized for develope
## Usage Instructions
Integrate Reducto Parse into the workflow. Can extract text from uploaded PDF documents, or from a URL.
Integrate Reducto Parse into the workflow. Can extract text from uploaded PDF documents or file references.
@@ -37,13 +37,12 @@ Integrate Reducto Parse into the workflow. Can extract text from uploaded PDF do
### `reducto_parser`
Parse PDF documents using Reducto OCR API
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `filePath` | string | Yes | URL to a PDF document to be processed |
| `filePath` | string | No | URL to a PDF document to be processed |
| `file` | file | No | Document file to be processed |
| `fileUpload` | object | No | File upload data from file-upload component |
| `pages` | array | No | Specific pages to process \(1-indexed page numbers\) |
| `tableOutputFormat` | string | No | Table output format \(html or markdown\). Defaults to markdown. |
@@ -51,13 +50,6 @@ Parse PDF documents using Reducto OCR API
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `job_id` | string | Unique identifier for the processing job |
| `duration` | number | Processing time in seconds |
| `usage` | json | Resource consumption data |
| `result` | json | Parsed document content with chunks and blocks |
| `pdf_url` | string | Storage URL of converted PDF |
| `studio_link` | string | Link to Reducto studio interface |
This tool does not produce any outputs.

View File

@@ -78,6 +78,7 @@ Retrieve an object from an AWS S3 bucket
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `url` | string | Pre-signed URL for downloading the S3 object |
| `file` | file | Downloaded file stored in execution files |
| `metadata` | object | File metadata including type, size, name, and last modified date |
### `s3_list_objects`

View File

@@ -62,7 +62,7 @@ Send an email using SendGrid API
| `bcc` | string | No | BCC email address |
| `replyTo` | string | No | Reply-to email address |
| `replyToName` | string | No | Reply-to name |
| `attachments` | file[] | No | Files to attach to the email as an array of attachment objects |
| `attachments` | file[] | No | Files to attach to the email \(UserFile objects\) |
| `templateId` | string | No | SendGrid template ID to use |
| `dynamicTemplateData` | json | No | JSON object of dynamic template data |

View File

@@ -97,6 +97,7 @@ Download a file from a remote SFTP server
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the download was successful |
| `file` | file | Downloaded file stored in execution files |
| `fileName` | string | Name of the downloaded file |
| `content` | string | File content \(text or base64 encoded\) |
| `size` | number | File size in bytes |

View File

@@ -144,6 +144,7 @@ Send messages to Slack channels or direct messages. Supports Slack mrkdwn format
| `ts` | string | Message timestamp |
| `channel` | string | Channel ID where message was sent |
| `fileCount` | number | Number of files uploaded \(when files are attached\) |
| `files` | file[] | Files attached to the message |
### `slack_canvas`

View File

@@ -170,6 +170,7 @@ Download a file from a remote SSH server
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `downloaded` | boolean | Whether the file was downloaded successfully |
| `file` | file | Downloaded file stored in execution files |
| `fileContent` | string | File content \(base64 encoded for binary files\) |
| `fileName` | string | Name of the downloaded file |
| `remotePath` | string | Source path on the remote server |

View File

@@ -6,7 +6,7 @@ description: Convert speech to text using AI
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="stt"
type="stt_v2"
color="#181C1E"
/>
@@ -50,8 +50,6 @@ Transcribe audio and video files to text using leading AI providers. Supports mu
### `stt_whisper`
Transcribe audio to text using OpenAI Whisper
#### Input
| Parameter | Type | Required | Description |
@@ -71,22 +69,10 @@ Transcribe audio to text using OpenAI Whisper
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `transcript` | string | Full transcribed text |
| `segments` | array | Timestamped segments |
| ↳ `text` | string | Transcribed text for this segment |
| ↳ `start` | number | Start time in seconds |
| ↳ `end` | number | End time in seconds |
| ↳ `speaker` | string | Speaker identifier \(if diarization enabled\) |
| ↳ `confidence` | number | Confidence score \(0-1\) |
| `language` | string | Detected or specified language |
| `duration` | number | Audio duration in seconds |
This tool does not produce any outputs.
### `stt_deepgram`
Transcribe audio to text using Deepgram
#### Input
| Parameter | Type | Required | Description |
@@ -103,23 +89,10 @@ Transcribe audio to text using Deepgram
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `transcript` | string | Full transcribed text |
| `segments` | array | Timestamped segments with speaker labels |
| ↳ `text` | string | Transcribed text for this segment |
| ↳ `start` | number | Start time in seconds |
| ↳ `end` | number | End time in seconds |
| ↳ `speaker` | string | Speaker identifier \(if diarization enabled\) |
| ↳ `confidence` | number | Confidence score \(0-1\) |
| `language` | string | Detected or specified language |
| `duration` | number | Audio duration in seconds |
| `confidence` | number | Overall confidence score |
This tool does not produce any outputs.
### `stt_elevenlabs`
Transcribe audio to text using ElevenLabs
#### Input
| Parameter | Type | Required | Description |
@@ -135,18 +108,10 @@ Transcribe audio to text using ElevenLabs
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `transcript` | string | Full transcribed text |
| `segments` | array | Timestamped segments |
| `language` | string | Detected or specified language |
| `duration` | number | Audio duration in seconds |
| `confidence` | number | Overall confidence score |
This tool does not produce any outputs.
### `stt_assemblyai`
Transcribe audio to text using AssemblyAI with advanced NLP features
#### Input
| Parameter | Type | Required | Description |
@@ -167,35 +132,10 @@ Transcribe audio to text using AssemblyAI with advanced NLP features
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `transcript` | string | Full transcribed text |
| `segments` | array | Timestamped segments with speaker labels |
| ↳ `text` | string | Transcribed text for this segment |
| ↳ `start` | number | Start time in seconds |
| ↳ `end` | number | End time in seconds |
| ↳ `speaker` | string | Speaker identifier \(if diarization enabled\) |
| ↳ `confidence` | number | Confidence score \(0-1\) |
| `language` | string | Detected or specified language |
| `duration` | number | Audio duration in seconds |
| `confidence` | number | Overall confidence score |
| `sentiment` | array | Sentiment analysis results |
| ↳ `text` | string | Text that was analyzed |
| ↳ `sentiment` | string | Sentiment \(POSITIVE, NEGATIVE, NEUTRAL\) |
| ↳ `confidence` | number | Confidence score |
| ↳ `start` | number | Start time in milliseconds |
| ↳ `end` | number | End time in milliseconds |
| `entities` | array | Detected entities |
| ↳ `entity_type` | string | Entity type \(e.g., person_name, location, organization\) |
| ↳ `text` | string | Entity text |
| ↳ `start` | number | Start time in milliseconds |
| ↳ `end` | number | End time in milliseconds |
| `summary` | string | Auto-generated summary |
This tool does not produce any outputs.
### `stt_gemini`
Transcribe audio to text using Google Gemini with multimodal capabilities
#### Input
| Parameter | Type | Required | Description |
@@ -211,12 +151,6 @@ Transcribe audio to text using Google Gemini with multimodal capabilities
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `transcript` | string | Full transcribed text |
| `segments` | array | Timestamped segments |
| `language` | string | Detected or specified language |
| `duration` | number | Audio duration in seconds |
| `confidence` | number | Overall confidence score |
This tool does not produce any outputs.

View File

@@ -354,6 +354,7 @@ Send documents (PDF, ZIP, DOC, etc.) to Telegram channels or users through the T
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `message` | string | Success or error message |
| `files` | file[] | Files attached to the message |
| `data` | object | Telegram message data including document |
| ↳ `message_id` | number | Unique Telegram message identifier |
| ↳ `from` | object | Information about the sender |

View File

@@ -6,7 +6,7 @@ description: Extract text, tables, and forms from documents
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="textract"
type="textract_v2"
color="linear-gradient(135deg, #055F4E 0%, #56C0A7 100%)"
/>
@@ -35,8 +35,6 @@ Integrate AWS Textract into your workflow to extract text, tables, forms, and ke
### `textract_parser`
Parse documents using AWS Textract OCR and document analysis
#### Input
| Parameter | Type | Required | Description |
@@ -46,8 +44,8 @@ Parse documents using AWS Textract OCR and document analysis
| `region` | string | Yes | AWS region for Textract service \(e.g., us-east-1\) |
| `processingMode` | string | No | Document type: single-page or multi-page. Defaults to single-page. |
| `filePath` | string | No | URL to a document to be processed \(JPEG, PNG, or single-page PDF\). |
| `file` | file | No | Document file to be processed \(JPEG, PNG, or single-page PDF\). |
| `s3Uri` | string | No | S3 URI for multi-page processing \(s3://bucket/key\). |
| `fileUpload` | object | No | File upload data from file-upload component |
| `featureTypes` | array | No | Feature types to detect: TABLES, FORMS, QUERIES, SIGNATURES, LAYOUT. If not specified, only text detection is performed. |
| `items` | string | No | Feature type |
| `queries` | array | No | Custom queries to extract specific information. Only used when featureTypes includes QUERIES. |
@@ -58,39 +56,6 @@ Parse documents using AWS Textract OCR and document analysis
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `blocks` | array | Array of Block objects containing detected text, tables, forms, and other elements |
| ↳ `BlockType` | string | Type of block \(PAGE, LINE, WORD, TABLE, CELL, KEY_VALUE_SET, etc.\) |
| ↳ `Id` | string | Unique identifier for the block |
| ↳ `Text` | string | The text content \(for LINE and WORD blocks\) |
| ↳ `TextType` | string | Type of text \(PRINTED or HANDWRITING\) |
| ↳ `Confidence` | number | Confidence score \(0-100\) |
| ↳ `Page` | number | Page number |
| ↳ `Geometry` | object | Location and bounding box information |
| ↳ `BoundingBox` | object | Height as ratio of document height |
| ↳ `Height` | number | Height as ratio of document height |
| ↳ `Left` | number | Left position as ratio of document width |
| ↳ `Top` | number | Top position as ratio of document height |
| ↳ `Width` | number | Width as ratio of document width |
| ↳ `Polygon` | array | Polygon coordinates |
| ↳ `X` | number | X coordinate |
| ↳ `Y` | number | Y coordinate |
| ↳ `Relationships` | array | Relationships to other blocks |
| ↳ `Type` | string | Relationship type \(CHILD, VALUE, ANSWER, etc.\) |
| ↳ `Ids` | array | IDs of related blocks |
| ↳ `EntityTypes` | array | Entity types for KEY_VALUE_SET \(KEY or VALUE\) |
| ↳ `SelectionStatus` | string | For checkboxes: SELECTED or NOT_SELECTED |
| ↳ `RowIndex` | number | Row index for table cells |
| ↳ `ColumnIndex` | number | Column index for table cells |
| ↳ `RowSpan` | number | Row span for merged cells |
| ↳ `ColumnSpan` | number | Column span for merged cells |
| ↳ `Query` | object | Query information for QUERY blocks |
| ↳ `Text` | string | Query text |
| ↳ `Alias` | string | Query alias |
| ↳ `Pages` | array | Pages to search |
| `documentMetadata` | object | Metadata about the analyzed document |
| ↳ `pages` | number | Number of pages in the document |
| `modelVersion` | string | Version of the Textract model used for processing |
This tool does not produce any outputs.

View File

@@ -122,6 +122,7 @@ Retrieve call recording information and transcription (if enabled via TwiML).
| `channels` | number | Number of channels \(1 for mono, 2 for dual\) |
| `source` | string | How the recording was created |
| `mediaUrl` | string | URL to download the recording media file |
| `file` | file | Downloaded recording media file |
| `price` | string | Cost of the recording |
| `priceUnit` | string | Currency of the price |
| `uri` | string | Relative URI of the recording resource |

View File

@@ -75,6 +75,7 @@ Download files uploaded in Typeform responses
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `fileUrl` | string | Direct download URL for the uploaded file |
| `file` | file | Downloaded file stored in execution files |
| `contentType` | string | MIME type of the uploaded file |
| `filename` | string | Original filename of the uploaded file |

View File

@@ -57,14 +57,14 @@ Generate videos using Runway Gen-4 with world consistency and visual references
| `duration` | number | No | Video duration in seconds \(5 or 10, default: 5\) |
| `aspectRatio` | string | No | Aspect ratio: 16:9 \(landscape\), 9:16 \(portrait\), or 1:1 \(square\) |
| `resolution` | string | No | Video resolution \(720p output\). Note: Gen-4 Turbo outputs at 720p natively |
| `visualReference` | json | Yes | Reference image REQUIRED for Gen-4 \(UserFile object\). Gen-4 only supports image-to-video, not text-only generation |
| `visualReference` | file | Yes | Reference image REQUIRED for Gen-4 \(UserFile object\). Gen-4 only supports image-to-video, not text-only generation |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `videoUrl` | string | Generated video URL |
| `videoFile` | json | Video file object with metadata |
| `videoFile` | file | Video file object with metadata |
| `duration` | number | Video duration in seconds |
| `width` | number | Video width in pixels |
| `height` | number | Video height in pixels |
@@ -93,7 +93,7 @@ Generate videos using Google Veo 3/3.1 with native audio generation
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `videoUrl` | string | Generated video URL |
| `videoFile` | json | Video file object with metadata |
| `videoFile` | file | Video file object with metadata |
| `duration` | number | Video duration in seconds |
| `width` | number | Video width in pixels |
| `height` | number | Video height in pixels |
@@ -123,7 +123,7 @@ Generate videos using Luma Dream Machine with advanced camera controls
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `videoUrl` | string | Generated video URL |
| `videoFile` | json | Video file object with metadata |
| `videoFile` | file | Video file object with metadata |
| `duration` | number | Video duration in seconds |
| `width` | number | Video width in pixels |
| `height` | number | Video height in pixels |
@@ -151,7 +151,7 @@ Generate videos using MiniMax Hailuo through MiniMax Platform API with advanced
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `videoUrl` | string | Generated video URL |
| `videoFile` | json | Video file object with metadata |
| `videoFile` | file | Video file object with metadata |
| `duration` | number | Video duration in seconds |
| `width` | number | Video width in pixels |
| `height` | number | Video height in pixels |
@@ -181,7 +181,7 @@ Generate videos using Fal.ai platform with access to multiple models including V
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `videoUrl` | string | Generated video URL |
| `videoFile` | json | Video file object with metadata |
| `videoFile` | file | Video file object with metadata |
| `duration` | number | Video duration in seconds |
| `width` | number | Video width in pixels |
| `height` | number | Video height in pixels |

View File

@@ -6,7 +6,7 @@ description: Analyze images with vision models
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="vision"
type="vision_v2"
color="#4D5FFF"
/>
@@ -35,8 +35,6 @@ Integrate Vision into the workflow. Can analyze images with vision models.
### `vision_tool`
Process and analyze images using advanced vision models. Capable of understanding image content, extracting text, identifying objects, and providing detailed visual descriptions.
#### Input
| Parameter | Type | Required | Description |
@@ -49,14 +47,6 @@ Process and analyze images using advanced vision models. Capable of understandin
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `content` | string | The analyzed content and description of the image |
| `model` | string | The vision model that was used for analysis |
| `tokens` | number | Total tokens used for the analysis |
| `usage` | object | Detailed token usage breakdown |
| ↳ `input_tokens` | number | Tokens used for input processing |
| ↳ `output_tokens` | number | Tokens used for response generation |
| ↳ `total_tokens` | number | Total tokens consumed |
This tool does not produce any outputs.

View File

@@ -335,6 +335,7 @@ Get all recordings for a specific Zoom meeting
| `meetingId` | string | Yes | The meeting ID or meeting UUID \(e.g., "1234567890" or "4444AAABBBccccc12345=="\) |
| `includeFolderItems` | boolean | No | Include items within a folder |
| `ttl` | number | No | Time to live for download URLs in seconds \(max 604800\) |
| `downloadFiles` | boolean | No | Download recording files into file outputs |
#### Output
@@ -364,6 +365,7 @@ Get all recordings for a specific Zoom meeting
| ↳ `download_url` | string | URL to download the recording |
| ↳ `status` | string | Recording status |
| ↳ `recording_type` | string | Type of recording \(shared_screen, audio_only, etc.\) |
| `files` | file[] | Downloaded recording files |
### `zoom_delete_recording`

View File

@@ -6,7 +6,7 @@ description: Leer y analizar múltiples archivos
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="file"
type="file_v3"
color="#40916C"
/>

View File

@@ -6,7 +6,7 @@ description: Interactúa con transcripciones y grabaciones de reuniones de Firef
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="fireflies"
type="fireflies_v2"
color="#100730"
/>

View File

@@ -6,7 +6,7 @@ description: Extraer texto de documentos PDF
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="mistral_parse"
type="mistral_parse_v3"
color="#000000"
/>

View File

@@ -6,7 +6,7 @@ description: Lire et analyser plusieurs fichiers
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="file"
type="file_v3"
color="#40916C"
/>

View File

@@ -7,7 +7,7 @@ description: Interagissez avec les transcriptions et enregistrements de réunion
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="fireflies"
type="fireflies_v2"
color="#100730"
/>

View File

@@ -6,7 +6,7 @@ description: Extraire du texte à partir de documents PDF
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="mistral_parse"
type="mistral_parse_v3"
color="#000000"
/>

View File

@@ -6,7 +6,7 @@ description: 複数のファイルを読み込んで解析する
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="file"
type="file_v3"
color="#40916C"
/>

View File

@@ -6,7 +6,7 @@ description: Fireflies.aiの会議文字起こしと録画を操作
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="fireflies"
type="fireflies_v2"
color="#100730"
/>

View File

@@ -6,7 +6,7 @@ description: PDFドキュメントからテキストを抽出する
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="mistral_parse"
type="mistral_parse_v3"
color="#000000"
/>

View File

@@ -6,7 +6,7 @@ description: 读取并解析多个文件
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="file"
type="file_v3"
color="#40916C"
/>

View File

@@ -6,7 +6,7 @@ description: 与 Fireflies.ai 会议转录和录音进行交互
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="fireflies"
type="fireflies_v2"
color="#100730"
/>

View File

@@ -6,7 +6,7 @@ description: 从 PDF 文档中提取文本
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="mistral_parse"
type="mistral_parse_v3"
color="#000000"
/>

View File

@@ -1,7 +1,7 @@
'use client'
import { useBrandConfig } from '@/lib/branding/branding'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { useBrandConfig } from '@/ee/whitelabeling'
export interface SupportFooterProps {
/** Position style - 'fixed' for pages without AuthLayout, 'absolute' for pages with AuthLayout */

View File

@@ -7,10 +7,10 @@ import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { GithubIcon } from '@/components/icons'
import { useBrandConfig } from '@/lib/branding/branding'
import { isHosted } from '@/lib/core/config/feature-flags'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import { getFormattedGitHubStars } from '@/app/(landing)/actions/github'
import { useBrandConfig } from '@/ee/whitelabeling'
import { useBrandedButtonClass } from '@/hooks/use-branded-button-class'
const logger = createLogger('nav')

View File

@@ -14,7 +14,6 @@ import {
parseWorkflowSSEChunk,
} from '@/lib/a2a/utils'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { getBrandConfig } from '@/lib/branding/branding'
import { acquireLock, getRedisClient, releaseLock } from '@/lib/core/config/redis'
import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server'
import { SSE_HEADERS } from '@/lib/core/utils/sse'
@@ -35,6 +34,7 @@ import {
type PushNotificationSetParams,
type TaskIdParams,
} from '@/app/api/a2a/serve/[agentId]/utils'
import { getBrandConfig } from '@/ee/whitelabeling'
const logger = createLogger('A2AServeAPI')

View File

@@ -21,7 +21,8 @@ export async function GET(request: NextRequest) {
const accessToken = searchParams.get('accessToken')
const pageId = searchParams.get('pageId')
const providedCloudId = searchParams.get('cloudId')
const limit = searchParams.get('limit') || '25'
const limit = searchParams.get('limit') || '50'
const cursor = searchParams.get('cursor')
if (!domain) {
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
@@ -47,7 +48,12 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/attachments?limit=${limit}`
const queryParams = new URLSearchParams()
queryParams.append('limit', String(Math.min(Number(limit), 250)))
if (cursor) {
queryParams.append('cursor', cursor)
}
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/attachments?${queryParams.toString()}`
const response = await fetch(url, {
method: 'GET',
@@ -77,9 +83,20 @@ export async function GET(request: NextRequest) {
fileSize: attachment.fileSize || 0,
mediaType: attachment.mediaType || '',
downloadUrl: attachment.downloadLink || attachment._links?.download || '',
status: attachment.status ?? null,
webuiUrl: attachment._links?.webui ?? null,
pageId: attachment.pageId ?? null,
blogPostId: attachment.blogPostId ?? null,
comment: attachment.comment ?? null,
version: attachment.version ?? null,
}))
return NextResponse.json({ attachments })
return NextResponse.json({
attachments,
nextCursor: data._links?.next
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
: null,
})
} catch (error) {
logger.error('Error listing Confluence attachments:', error)
return NextResponse.json(

View File

@@ -0,0 +1,285 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
import { getConfluenceCloudId } from '@/tools/confluence/utils'
const logger = createLogger('ConfluenceBlogPostsAPI')
export const dynamic = 'force-dynamic'
const getBlogPostSchema = z
.object({
domain: z.string().min(1, 'Domain is required'),
accessToken: z.string().min(1, 'Access token is required'),
cloudId: z.string().optional(),
blogPostId: z.string().min(1, 'Blog post ID is required'),
bodyFormat: z.string().optional(),
})
.refine(
(data) => {
const validation = validateAlphanumericId(data.blogPostId, 'blogPostId', 255)
return validation.isValid
},
(data) => {
const validation = validateAlphanumericId(data.blogPostId, 'blogPostId', 255)
return { message: validation.error || 'Invalid blog post ID', path: ['blogPostId'] }
}
)
const createBlogPostSchema = z.object({
domain: z.string().min(1, 'Domain is required'),
accessToken: z.string().min(1, 'Access token is required'),
cloudId: z.string().optional(),
spaceId: z.string().min(1, 'Space ID is required'),
title: z.string().min(1, 'Title is required'),
content: z.string().min(1, 'Content is required'),
status: z.enum(['current', 'draft']).optional(),
})
/**
* List all blog posts or get a specific blog post
*/
export async function GET(request: NextRequest) {
try {
const auth = await checkSessionOrInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const { searchParams } = new URL(request.url)
const domain = searchParams.get('domain')
const accessToken = searchParams.get('accessToken')
const providedCloudId = searchParams.get('cloudId')
const limit = searchParams.get('limit') || '25'
const status = searchParams.get('status')
const sortOrder = searchParams.get('sort')
const cursor = searchParams.get('cursor')
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 })
}
const queryParams = new URLSearchParams()
queryParams.append('limit', String(Math.min(Number(limit), 250)))
if (status) {
queryParams.append('status', status)
}
if (sortOrder) {
queryParams.append('sort', sortOrder)
}
if (cursor) {
queryParams.append('cursor', cursor)
}
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/blogposts?${queryParams.toString()}`
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 blog posts (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
const blogPosts = (data.results || []).map((post: any) => ({
id: post.id,
title: post.title,
status: post.status ?? null,
spaceId: post.spaceId ?? null,
authorId: post.authorId ?? null,
createdAt: post.createdAt ?? null,
version: post.version ?? null,
webUrl: post._links?.webui ?? null,
}))
return NextResponse.json({
blogPosts,
nextCursor: data._links?.next
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
: null,
})
} catch (error) {
logger.error('Error listing blog posts:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}
/**
* Get a specific blog post by 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()
// Check if this is a create or get request
if (body.title && body.content && body.spaceId) {
// Create blog post
const validation = createBlogPostSchema.safeParse(body)
if (!validation.success) {
const firstError = validation.error.errors[0]
return NextResponse.json({ error: firstError.message }, { status: 400 })
}
const {
domain,
accessToken,
cloudId: providedCloudId,
spaceId,
title,
content,
status,
} = validation.data
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`
const createBody = {
spaceId,
status: status || 'current',
title,
body: {
representation: 'storage',
value: content,
},
}
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 blog post (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
return NextResponse.json({
id: data.id,
title: data.title,
spaceId: data.spaceId,
webUrl: data._links?.webui ?? null,
})
}
// Get blog post by ID
const validation = getBlogPostSchema.safeParse(body)
if (!validation.success) {
const firstError = validation.error.errors[0]
return NextResponse.json({ error: firstError.message }, { status: 400 })
}
const {
domain,
accessToken,
cloudId: providedCloudId,
blogPostId,
bodyFormat,
} = validation.data
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()
if (bodyFormat) {
queryParams.append('body-format', bodyFormat)
}
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/blogposts/${blogPostId}${queryParams.toString() ? `?${queryParams.toString()}` : ''}`
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 blog post (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
return NextResponse.json({
id: data.id,
title: data.title,
status: data.status ?? null,
spaceId: data.spaceId ?? null,
authorId: data.authorId ?? null,
createdAt: data.createdAt ?? null,
version: data.version ?? null,
body: data.body ?? null,
webUrl: data._links?.webui ?? null,
})
} catch (error) {
logger.error('Error with blog post operation:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -105,6 +105,8 @@ export async function GET(request: NextRequest) {
const pageId = searchParams.get('pageId')
const providedCloudId = searchParams.get('cloudId')
const limit = searchParams.get('limit') || '25'
const bodyFormat = searchParams.get('bodyFormat') || 'storage'
const cursor = searchParams.get('cursor')
if (!domain) {
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
@@ -130,7 +132,13 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/footer-comments?limit=${limit}`
const queryParams = new URLSearchParams()
queryParams.append('limit', String(Math.min(Number(limit), 250)))
queryParams.append('body-format', bodyFormat)
if (cursor) {
queryParams.append('cursor', cursor)
}
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/footer-comments?${queryParams.toString()}`
const response = await fetch(url, {
method: 'GET',
@@ -154,14 +162,31 @@ export async function GET(request: NextRequest) {
const data = await response.json()
const comments = (data.results || []).map((comment: any) => ({
id: comment.id,
body: comment.body?.storage?.value || comment.body?.view?.value || '',
createdAt: comment.createdAt || '',
authorId: comment.authorId || '',
}))
const comments = (data.results || []).map((comment: any) => {
const bodyValue = comment.body?.storage?.value || comment.body?.view?.value || ''
return {
id: comment.id,
body: {
value: bodyValue,
representation: bodyFormat,
},
createdAt: comment.createdAt || '',
authorId: comment.authorId || '',
status: comment.status ?? null,
title: comment.title ?? null,
pageId: comment.pageId ?? null,
blogPostId: comment.blogPostId ?? null,
parentCommentId: comment.parentCommentId ?? null,
version: comment.version ?? null,
}
})
return NextResponse.json({ comments })
return NextResponse.json({
comments,
nextCursor: data._links?.next
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
: null,
})
} catch (error) {
logger.error('Error listing Confluence comments:', error)
return NextResponse.json(

View File

@@ -22,6 +22,7 @@ export async function POST(request: NextRequest) {
cloudId: providedCloudId,
pageId,
labelName,
prefix: labelPrefix,
} = await request.json()
if (!domain) {
@@ -52,12 +53,14 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/labels`
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/rest/api/content/${pageId}/label`
const body = {
prefix: 'global',
name: labelName,
}
const body = [
{
prefix: labelPrefix || 'global',
name: labelName,
},
]
const response = await fetch(url, {
method: 'POST',
@@ -82,7 +85,14 @@ export async function POST(request: NextRequest) {
}
const data = await response.json()
return NextResponse.json({ ...data, pageId, labelName })
const addedLabel = data.results?.[0] || data[0] || data
return NextResponse.json({
id: addedLabel.id ?? '',
name: addedLabel.name ?? labelName,
prefix: addedLabel.prefix ?? labelPrefix ?? 'global',
pageId,
labelName,
})
} catch (error) {
logger.error('Error adding Confluence label:', error)
return NextResponse.json(
@@ -105,6 +115,8 @@ export async function GET(request: NextRequest) {
const accessToken = searchParams.get('accessToken')
const pageId = searchParams.get('pageId')
const providedCloudId = searchParams.get('cloudId')
const limit = searchParams.get('limit') || '25'
const cursor = searchParams.get('cursor')
if (!domain) {
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
@@ -130,7 +142,12 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/labels`
const queryParams = new URLSearchParams()
queryParams.append('limit', String(Math.min(Number(limit), 250)))
if (cursor) {
queryParams.append('cursor', cursor)
}
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/labels?${queryParams.toString()}`
const response = await fetch(url, {
method: 'GET',
@@ -160,7 +177,12 @@ export async function GET(request: NextRequest) {
prefix: label.prefix || 'global',
}))
return NextResponse.json({ labels })
return NextResponse.json({
labels,
nextCursor: data._links?.next
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
: null,
})
} catch (error) {
logger.error('Error listing Confluence labels:', error)
return NextResponse.json(

View File

@@ -0,0 +1,96 @@
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('ConfluencePageAncestorsAPI')
export const dynamic = 'force-dynamic'
/**
* Get ancestors (parent pages) of a specific Confluence page.
* Uses GET /wiki/api/v2/pages/{id}/ancestors
*/
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 = 25 } = 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)))
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/ancestors?${queryParams.toString()}`
logger.info(`Fetching ancestors 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 ancestors (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
const ancestors = (data.results || []).map((page: any) => ({
id: page.id,
title: page.title,
status: page.status ?? null,
spaceId: page.spaceId ?? null,
webUrl: page._links?.webui ?? null,
}))
return NextResponse.json({
ancestors,
pageId,
})
} catch (error) {
logger.error('Error getting page ancestors:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,104 @@
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('ConfluencePageChildrenAPI')
export const dynamic = 'force-dynamic'
/**
* Get child pages of a specific Confluence page.
* Uses GET /wiki/api/v2/pages/{id}/children
*/
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}/children?${queryParams.toString()}`
logger.info(`Fetching child pages 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 child pages (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
const children = (data.results || []).map((page: any) => ({
id: page.id,
title: page.title,
status: page.status ?? null,
spaceId: page.spaceId ?? null,
childPosition: page.childPosition ?? null,
webUrl: page._links?.webui ?? null,
}))
return NextResponse.json({
children,
parentId: pageId,
nextCursor: data._links?.next
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
: null,
})
} catch (error) {
logger.error('Error getting child pages:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,365 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
import { getConfluenceCloudId } from '@/tools/confluence/utils'
const logger = createLogger('ConfluencePagePropertiesAPI')
export const dynamic = 'force-dynamic'
const createPropertySchema = z.object({
domain: z.string().min(1, 'Domain is required'),
accessToken: z.string().min(1, 'Access token is required'),
cloudId: z.string().optional(),
pageId: z.string().min(1, 'Page ID is required'),
key: z.string().min(1, 'Property key is required'),
value: z.any(),
})
const updatePropertySchema = z.object({
domain: z.string().min(1, 'Domain is required'),
accessToken: z.string().min(1, 'Access token is required'),
cloudId: z.string().optional(),
pageId: z.string().min(1, 'Page ID is required'),
propertyId: z.string().min(1, 'Property ID is required'),
key: z.string().min(1, 'Property key is required'),
value: z.any(),
versionNumber: z.number().min(1, 'Version number is required'),
})
const deletePropertySchema = z.object({
domain: z.string().min(1, 'Domain is required'),
accessToken: z.string().min(1, 'Access token is required'),
cloudId: z.string().optional(),
pageId: z.string().min(1, 'Page ID is required'),
propertyId: z.string().min(1, 'Property ID is required'),
})
/**
* List all content properties on a page.
*/
export async function GET(request: NextRequest) {
try {
const auth = await checkSessionOrInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const { searchParams } = new URL(request.url)
const domain = searchParams.get('domain')
const accessToken = searchParams.get('accessToken')
const pageId = searchParams.get('pageId')
const providedCloudId = searchParams.get('cloudId')
const limit = searchParams.get('limit') || '50'
const cursor = searchParams.get('cursor')
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(Number(limit), 250)))
if (cursor) {
queryParams.append('cursor', cursor)
}
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/properties?${queryParams.toString()}`
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 page 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,
version: prop.version ?? null,
}))
return NextResponse.json({
properties,
pageId,
nextCursor: data._links?.next
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
: null,
})
} catch (error) {
logger.error('Error listing page properties:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}
/**
* Create a new content property on a page.
*/
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 validation = createPropertySchema.safeParse(body)
if (!validation.success) {
const firstError = validation.error.errors[0]
return NextResponse.json({ error: firstError.message }, { status: 400 })
}
const { domain, accessToken, cloudId: providedCloudId, pageId, key, value } = validation.data
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 url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/properties`
const response = await fetch(url, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({ key, 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 page property (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
return NextResponse.json({
id: data.id,
key: data.key,
value: data.value,
version: data.version,
pageId,
})
} catch (error) {
logger.error('Error creating page property:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}
/**
* Update a content property on a page.
*/
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 validation = updatePropertySchema.safeParse(body)
if (!validation.success) {
const firstError = validation.error.errors[0]
return NextResponse.json({ error: firstError.message }, { status: 400 })
}
const {
domain,
accessToken,
cloudId: providedCloudId,
pageId,
propertyId,
key,
value,
versionNumber,
} = validation.data
const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255)
if (!pageIdValidation.isValid) {
return NextResponse.json({ error: pageIdValidation.error }, { status: 400 })
}
const propertyIdValidation = validateAlphanumericId(propertyId, 'propertyId', 255)
if (!propertyIdValidation.isValid) {
return NextResponse.json({ error: propertyIdValidation.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/pages/${pageId}/properties/${propertyId}`
const response = await fetch(url, {
method: 'PUT',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
key,
value,
version: { number: versionNumber },
}),
})
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 page property (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
return NextResponse.json({
id: data.id,
key: data.key,
value: data.value,
version: data.version,
pageId,
})
} catch (error) {
logger.error('Error updating page property:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}
/**
* Delete a content property from a page.
*/
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 validation = deletePropertySchema.safeParse(body)
if (!validation.success) {
const firstError = validation.error.errors[0]
return NextResponse.json({ error: firstError.message }, { status: 400 })
}
const { domain, accessToken, cloudId: providedCloudId, pageId, propertyId } = validation.data
const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255)
if (!pageIdValidation.isValid) {
return NextResponse.json({ error: pageIdValidation.error }, { status: 400 })
}
const propertyIdValidation = validateAlphanumericId(propertyId, 'propertyId', 255)
if (!propertyIdValidation.isValid) {
return NextResponse.json({ error: propertyIdValidation.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/pages/${pageId}/properties/${propertyId}`
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 page property (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
return NextResponse.json({ propertyId, pageId, deleted: true })
} catch (error) {
logger.error('Error deleting page property:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,151 @@
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('ConfluencePageVersionsAPI')
export const dynamic = 'force-dynamic'
/**
* List all versions of a page or get a specific version.
* Uses GET /wiki/api/v2/pages/{id}/versions
* and GET /wiki/api/v2/pages/{page-id}/versions/{version-number}
*/
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,
versionNumber,
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 })
}
// If versionNumber is provided, get specific version
if (versionNumber !== undefined && versionNumber !== null) {
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/versions/${versionNumber}`
logger.info(`Fetching version ${versionNumber} 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 version (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
return NextResponse.json({
version: {
number: data.number,
message: data.message ?? null,
minorEdit: data.minorEdit ?? false,
authorId: data.authorId ?? null,
createdAt: data.createdAt ?? null,
},
pageId,
})
}
// List all versions
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}/versions?${queryParams.toString()}`
logger.info(`Fetching versions 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 list page versions (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
const versions = (data.results || []).map((version: any) => ({
number: version.number,
message: version.message ?? null,
minorEdit: version.minorEdit ?? false,
authorId: version.authorId ?? null,
createdAt: version.createdAt ?? null,
}))
return NextResponse.json({
versions,
pageId,
nextCursor: data._links?.next
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
: null,
})
} catch (error) {
logger.error('Error with page versions:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -62,6 +62,7 @@ const deletePageSchema = z
accessToken: z.string().min(1, 'Access token is required'),
cloudId: z.string().optional(),
pageId: z.string().min(1, 'Page ID is required'),
purge: z.boolean().optional(),
})
.refine(
(data) => {
@@ -98,7 +99,7 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}?expand=body.storage,body.view,body.atlas_doc_format`
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}?body-format=storage`
const response = await fetch(url, {
method: 'GET',
@@ -130,16 +131,18 @@ export async function POST(request: NextRequest) {
id: data.id,
title: data.title,
body: {
view: {
value:
data.body?.storage?.value ||
data.body?.view?.value ||
data.body?.atlas_doc_format?.value ||
data.content || // try alternative fields
data.description ||
`Content for page ${data.title}`, // fallback content
storage: {
value: data.body?.storage?.value ?? null,
representation: 'storage',
},
},
status: data.status ?? null,
spaceId: data.spaceId ?? null,
parentId: data.parentId ?? null,
authorId: data.authorId ?? null,
createdAt: data.createdAt ?? null,
version: data.version ?? null,
_links: data._links ?? null,
})
} catch (error) {
logger.error('Error fetching Confluence page:', error)
@@ -274,7 +277,7 @@ export async function DELETE(request: NextRequest) {
return NextResponse.json({ error: firstError.message }, { status: 400 })
}
const { domain, accessToken, cloudId: providedCloudId, pageId } = validation.data
const { domain, accessToken, cloudId: providedCloudId, pageId, purge } = validation.data
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
@@ -283,7 +286,12 @@ export async function DELETE(request: NextRequest) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}`
const queryParams = new URLSearchParams()
if (purge) {
queryParams.append('purge', 'true')
}
const queryString = queryParams.toString()
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}${queryString ? `?${queryString}` : ''}`
const response = await fetch(url, {
method: 'DELETE',

View File

@@ -32,7 +32,6 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'Access token is required' }, { status: 400 })
}
// Use provided cloudId or fetch it if not provided
const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken))
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
@@ -40,7 +39,6 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
// Build the URL with query parameters
const baseUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages`
const queryParams = new URLSearchParams()
@@ -57,7 +55,6 @@ export async function POST(request: NextRequest) {
logger.info(`Fetching Confluence pages from: ${url}`)
// Make the request to Confluence API with OAuth Bearer token
const response = await fetch(url, {
method: 'GET',
headers: {
@@ -79,7 +76,6 @@ export async function POST(request: NextRequest) {
} catch (e) {
logger.error('Could not parse error response as JSON:', e)
// Try to get the response text for more context
try {
const text = await response.text()
logger.error('Response text:', text)

View File

@@ -0,0 +1,120 @@
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('ConfluenceSearchInSpaceAPI')
export const dynamic = 'force-dynamic'
/**
* Search for content within a specific Confluence space using CQL.
*/
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,
spaceKey,
query,
cloudId: providedCloudId,
limit = 25,
contentType,
} = 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 (!spaceKey) {
return NextResponse.json({ error: 'Space key is required' }, { status: 400 })
}
const spaceKeyValidation = validateAlphanumericId(spaceKey, 'spaceKey', 255)
if (!spaceKeyValidation.isValid) {
return NextResponse.json({ error: spaceKeyValidation.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 escapeCqlValue = (value: string) => value.replace(/"/g, '\\"')
let cql = `space = "${escapeCqlValue(spaceKey)}"`
if (query) {
cql += ` AND text ~ "${escapeCqlValue(query)}"`
}
if (contentType) {
cql += ` AND type = "${escapeCqlValue(contentType)}"`
}
const searchParams = new URLSearchParams({
cql,
limit: String(Math.min(limit, 250)),
})
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/rest/api/search?${searchParams.toString()}`
logger.info(`Searching in space ${spaceKey} with CQL: ${cql}`)
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 search in space (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
const results = (data.results || []).map((result: any) => ({
id: result.content?.id ?? result.id,
title: result.content?.title ?? result.title,
type: result.content?.type ?? result.type,
status: result.content?.status ?? null,
url: result.url ?? result._links?.webui ?? '',
excerpt: result.excerpt ?? '',
lastModified: result.lastModified ?? null,
}))
return NextResponse.json({
results,
spaceKey,
totalSize: data.totalSize ?? results.length,
})
} catch (error) {
logger.error('Error searching in space:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -42,8 +42,10 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const escapeCqlValue = (value: string) => value.replace(/"/g, '\\"')
const searchParams = new URLSearchParams({
cql: `text ~ "${query}"`,
cql: `text ~ "${escapeCqlValue(query)}"`,
limit: limit.toString(),
})
@@ -70,13 +72,27 @@ export async function POST(request: NextRequest) {
const data = await response.json()
const results = (data.results || []).map((result: any) => ({
id: result.content?.id || result.id,
title: result.content?.title || result.title,
type: result.content?.type || result.type,
url: result.url || result._links?.webui || '',
excerpt: result.excerpt || '',
}))
const results = (data.results || []).map((result: any) => {
const spaceData = result.resultGlobalContainer || result.content?.space
return {
id: result.content?.id || result.id,
title: result.content?.title || result.title,
type: result.content?.type || result.type,
url: result.url || result._links?.webui || '',
excerpt: result.excerpt || '',
status: result.content?.status ?? null,
spaceKey: result.resultGlobalContainer?.key ?? result.content?.space?.key ?? null,
space: spaceData
? {
id: spaceData.id ?? null,
key: spaceData.key ?? null,
name: spaceData.name ?? spaceData.title ?? null,
}
: null,
lastModified: result.lastModified ?? result.content?.history?.lastUpdated?.when ?? null,
entityType: result.entityType ?? null,
}
})
return NextResponse.json({ results })
} catch (error) {

View File

@@ -0,0 +1,124 @@
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('ConfluenceSpaceBlogPostsAPI')
export const dynamic = 'force-dynamic'
/**
* List all blog posts in a specific Confluence space.
* Uses GET /wiki/api/v2/spaces/{id}/blogposts
*/
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 = 25,
status,
bodyFormat,
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 (status) {
queryParams.append('status', status)
}
if (bodyFormat) {
queryParams.append('body-format', bodyFormat)
}
if (cursor) {
queryParams.append('cursor', cursor)
}
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}/blogposts?${queryParams.toString()}`
logger.info(`Fetching blog posts in 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 blog posts in space (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
const blogPosts = (data.results || []).map((post: any) => ({
id: post.id,
title: post.title,
status: post.status ?? null,
spaceId: post.spaceId ?? null,
authorId: post.authorId ?? null,
createdAt: post.createdAt ?? null,
version: post.version ?? null,
body: post.body ?? null,
webUrl: post._links?.webui ?? null,
}))
return NextResponse.json({
blogPosts,
nextCursor: data._links?.next
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
: null,
})
} catch (error) {
logger.error('Error listing blog posts in space:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,125 @@
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('ConfluenceSpacePagesAPI')
export const dynamic = 'force-dynamic'
/**
* List all pages in a specific Confluence space.
* Uses GET /wiki/api/v2/spaces/{id}/pages
*/
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,
status,
bodyFormat,
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 (status) {
queryParams.append('status', status)
}
if (bodyFormat) {
queryParams.append('body-format', bodyFormat)
}
if (cursor) {
queryParams.append('cursor', cursor)
}
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces/${spaceId}/pages?${queryParams.toString()}`
logger.info(`Fetching pages in 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 pages in space (${response.status})`
return NextResponse.json({ error: errorMessage }, { status: response.status })
}
const data = await response.json()
const pages = (data.results || []).map((page: any) => ({
id: page.id,
title: page.title,
status: page.status ?? null,
spaceId: page.spaceId ?? null,
parentId: page.parentId ?? null,
authorId: page.authorId ?? null,
createdAt: page.createdAt ?? null,
version: page.version ?? null,
body: page.body ?? null,
webUrl: page._links?.webui ?? null,
}))
return NextResponse.json({
pages,
nextCursor: data._links?.next
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
: null,
})
} catch (error) {
logger.error('Error listing pages in space:', error)
return NextResponse.json(
{ error: (error as Error).message || 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -21,6 +21,7 @@ export async function GET(request: NextRequest) {
const accessToken = searchParams.get('accessToken')
const providedCloudId = searchParams.get('cloudId')
const limit = searchParams.get('limit') || '25'
const cursor = searchParams.get('cursor')
if (!domain) {
return NextResponse.json({ error: 'Domain is required' }, { status: 400 })
@@ -37,7 +38,12 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces?limit=${limit}`
const queryParams = new URLSearchParams()
queryParams.append('limit', String(Math.min(Number(limit), 250)))
if (cursor) {
queryParams.append('cursor', cursor)
}
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/spaces?${queryParams.toString()}`
const response = await fetch(url, {
method: 'GET',
@@ -67,9 +73,18 @@ export async function GET(request: NextRequest) {
key: space.key,
type: space.type,
status: space.status,
authorId: space.authorId ?? null,
createdAt: space.createdAt ?? null,
homepageId: space.homepageId ?? null,
description: space.description ?? null,
}))
return NextResponse.json({ spaces })
return NextResponse.json({
spaces,
nextCursor: data._links?.next
? new URL(data._links.next, 'https://placeholder').searchParams.get('cursor')
: null,
})
} catch (error) {
logger.error('Error listing Confluence spaces:', error)
return NextResponse.json(

View File

@@ -3,8 +3,8 @@
import Image from 'next/image'
import Link from 'next/link'
import { GithubIcon } from '@/components/icons'
import { useBrandConfig } from '@/lib/branding/branding'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { useBrandConfig } from '@/ee/whitelabeling'
interface ChatHeaderProps {
chatConfig: {

View File

@@ -1,8 +1,8 @@
'use client'
import Image from 'next/image'
import { useBrandConfig } from '@/lib/branding/branding'
import { inter } from '@/app/_styles/fonts/inter/inter'
import { useBrandConfig } from '@/ee/whitelabeling'
export function PoweredBySim() {
const brandConfig = useBrandConfig()

View File

@@ -2,9 +2,12 @@ import type { Metadata, Viewport } from 'next'
import Script from 'next/script'
import { PublicEnvScript } from 'next-runtime-env'
import { BrandedLayout } from '@/components/branded-layout'
import { generateThemeCSS } from '@/lib/branding/inject-theme'
import { generateBrandedMetadata, generateStructuredData } from '@/lib/branding/metadata'
import { PostHogProvider } from '@/app/_shell/providers/posthog-provider'
import {
generateBrandedMetadata,
generateStructuredData,
generateThemeCSS,
} from '@/ee/whitelabeling'
import '@/app/_styles/globals.css'
import { OneDollarStats } from '@/components/analytics/onedollarstats'
import { isReactGrabEnabled, isReactScanEnabled } from '@/lib/core/config/feature-flags'

View File

@@ -1,5 +1,5 @@
import type { MetadataRoute } from 'next'
import { getBrandConfig } from '@/lib/branding/branding'
import { getBrandConfig } from '@/ee/whitelabeling'
export default function manifest(): MetadataRoute.Manifest {
const brand = getBrandConfig()

View File

@@ -24,8 +24,8 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { useBrandConfig } from '@/lib/branding/branding'
import Nav from '@/app/(landing)/components/nav/nav'
import { useBrandConfig } from '@/ee/whitelabeling'
import type { ResumeStatus } from '@/executor/types'
interface ResumeLinks {

View File

@@ -74,6 +74,12 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
'write:label:confluence': 'Add and remove labels',
'search:confluence': 'Search Confluence content',
'readonly:content.attachment:confluence': 'View attachments',
'read:blogpost:confluence': 'View Confluence blog posts',
'write:blogpost:confluence': 'Create and update Confluence blog posts',
'read:content.property:confluence': 'View properties on Confluence content',
'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:me': 'Read profile information',
'database.read': 'Read database',
'database.write': 'Write to database',
@@ -358,6 +364,7 @@ export function OAuthRequiredModal({
logger.info('Linking OAuth2:', {
providerId,
requiredScopes,
hasNewScopes: newScopes.length > 0,
})
if (providerId === 'trello') {

View File

@@ -100,7 +100,7 @@ const BlockRow = memo(function BlockRow({
>
<div className='flex min-w-0 flex-1 items-center gap-[8px]'>
<div
className='relative flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center overflow-hidden rounded-[4px]'
className='flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center rounded-[4px]'
style={{ background: bgColor }}
>
{BlockIcon && <BlockIcon className='h-[9px] w-[9px] text-white' />}
@@ -276,7 +276,7 @@ const SubflowNodeRow = memo(function SubflowNodeRow({
>
<div className='flex min-w-0 flex-1 items-center gap-[8px]'>
<div
className='relative flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center overflow-hidden rounded-[4px]'
className='flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center rounded-[4px]'
style={{ background: bgColor }}
>
{BlockIcon && <BlockIcon className='h-[9px] w-[9px] text-white' />}

View File

@@ -19,11 +19,11 @@ import {
import { Input, Skeleton } from '@/components/ui'
import { signOut, useSession } from '@/lib/auth/auth-client'
import { ANONYMOUS_USER_ID } from '@/lib/auth/constants'
import { useBrandConfig } from '@/lib/branding/branding'
import { getEnv, isTruthy } from '@/lib/core/config/env'
import { isHosted } from '@/lib/core/config/feature-flags'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { useProfilePictureUpload } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/hooks/use-profile-picture-upload'
import { useBrandConfig } from '@/ee/whitelabeling'
import { useGeneralSettings, useUpdateGeneralSetting } from '@/hooks/queries/general-settings'
import { useUpdateUserProfile, useUserProfile } from '@/hooks/queries/user-profile'
import { clearUserData } from '@/stores'

View File

@@ -397,7 +397,7 @@ export function UsageIndicator({ onClick }: UsageIndicatorProps) {
return () => window.clearInterval(interval)
}, [isHovered, pillCount, startAnimationIndex])
if (isLoading) {
if (isLoading && !subscriptionData) {
return (
<div className='flex flex-shrink-0 flex-col gap-[8px] border-t px-[13.5px] pt-[8px] pb-[10px]'>
<div className='flex h-[18px] items-center justify-between'>

View File

@@ -75,6 +75,12 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
'search:confluence',
'read:me',
'offline_access',
'read:blogpost:confluence',
'write:blogpost:confluence',
'read:content.property:confluence',
'write:content.property:confluence',
'read:hierarchical-content:confluence',
'read:content.metadata:confluence',
],
placeholder: 'Select Confluence account',
required: true,
@@ -336,6 +342,7 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
ts: { type: 'string', description: 'Timestamp' },
pageId: { type: 'string', description: 'Page identifier' },
content: { type: 'string', description: 'Page content' },
body: { type: 'json', description: 'Page body with storage format' },
title: { type: 'string', description: 'Page title' },
url: { type: 'string', description: 'Page or resource URL' },
success: { type: 'boolean', description: 'Operation success status' },
@@ -373,31 +380,46 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
title: 'Operation',
type: 'dropdown',
options: [
// Page Operations
{ label: 'Read Page', id: 'read' },
{ label: 'Create Page', id: 'create' },
{ label: 'Update Page', id: 'update' },
{ label: 'Delete Page', id: 'delete' },
{ label: 'List Pages in Space', id: 'list_pages_in_space' },
{ label: 'Get Page Children', id: 'get_page_children' },
{ label: 'Get Page Ancestors', id: 'get_page_ancestors' },
// Version Operations
{ label: 'List Page Versions', id: 'list_page_versions' },
{ label: 'Get Page Version', id: 'get_page_version' },
// Page Property Operations
{ label: 'List Page Properties', id: 'list_page_properties' },
{ label: 'Create Page Property', id: 'create_page_property' },
// Search Operations
{ label: 'Search Content', id: 'search' },
{ label: 'Search in Space', id: 'search_in_space' },
// Blog Post Operations
{ label: 'List Blog Posts', id: 'list_blogposts' },
{ label: 'Get Blog Post', id: 'get_blogpost' },
{ label: 'Create Blog Post', id: 'create_blogpost' },
{ label: 'List Blog Posts in Space', id: 'list_blogposts_in_space' },
// Comment Operations
{ label: 'Create Comment', id: 'create_comment' },
{ label: 'List Comments', id: 'list_comments' },
{ label: 'Update Comment', id: 'update_comment' },
{ label: 'Delete Comment', id: 'delete_comment' },
// Attachment Operations
{ label: 'Upload Attachment', id: 'upload_attachment' },
{ label: 'List Attachments', id: 'list_attachments' },
{ label: 'Delete Attachment', id: 'delete_attachment' },
// Label Operations
{ label: 'List Labels', id: 'list_labels' },
{ label: 'Add Label', id: 'add_label' },
// Space Operations
{ label: 'Get Space', id: 'get_space' },
{ label: 'List Spaces', id: 'list_spaces' },
],
value: () => 'read',
},
{
id: 'domain',
title: 'Domain',
type: 'short-input',
placeholder: 'Enter Confluence domain (e.g., simstudio.atlassian.net)',
required: true,
},
{
id: 'credential',
title: 'Confluence Account',
@@ -426,10 +448,23 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
'search:confluence',
'read:me',
'offline_access',
'read:blogpost:confluence',
'write:blogpost:confluence',
'read:content.property:confluence',
'write:content.property:confluence',
'read:hierarchical-content:confluence',
'read:content.metadata:confluence',
],
placeholder: 'Select Confluence account',
required: true,
},
{
id: 'domain',
title: 'Domain',
type: 'short-input',
placeholder: 'Enter Confluence domain (e.g., simstudio.atlassian.net)',
required: true,
},
{
id: 'pageId',
title: 'Select Page',
@@ -439,6 +474,20 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
placeholder: 'Select Confluence page',
dependsOn: ['credential', 'domain'],
mode: 'basic',
condition: {
field: 'operation',
value: [
'list_pages_in_space',
'list_blogposts',
'get_blogpost',
'list_blogposts_in_space',
'search',
'search_in_space',
'get_space',
'list_spaces',
],
not: true,
},
required: {
field: 'operation',
value: [
@@ -450,6 +499,13 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
'list_attachments',
'list_labels',
'upload_attachment',
'add_label',
'get_page_children',
'get_page_ancestors',
'list_page_versions',
'get_page_version',
'list_page_properties',
'create_page_property',
],
},
},
@@ -460,6 +516,20 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
canonicalParamId: 'pageId',
placeholder: 'Enter Confluence page ID',
mode: 'advanced',
condition: {
field: 'operation',
value: [
'list_pages_in_space',
'list_blogposts',
'get_blogpost',
'list_blogposts_in_space',
'search',
'search_in_space',
'get_space',
'list_spaces',
],
not: true,
},
required: {
field: 'operation',
value: [
@@ -471,6 +541,13 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
'list_attachments',
'list_labels',
'upload_attachment',
'add_label',
'get_page_children',
'get_page_ancestors',
'list_page_versions',
'get_page_version',
'list_page_properties',
'create_page_property',
],
},
},
@@ -479,21 +556,64 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
title: 'Space ID',
type: 'short-input',
placeholder: 'Enter Confluence space ID',
required: { field: 'operation', value: ['create', 'get_space'] },
required: true,
condition: {
field: 'operation',
value: [
'create',
'get_space',
'list_pages_in_space',
'search_in_space',
'create_blogpost',
'list_blogposts_in_space',
],
},
},
{
id: 'blogPostId',
title: 'Blog Post ID',
type: 'short-input',
placeholder: 'Enter blog post ID',
required: true,
condition: { field: 'operation', value: 'get_blogpost' },
},
{
id: 'versionNumber',
title: 'Version Number',
type: 'short-input',
placeholder: 'Enter version number',
required: true,
condition: { field: 'operation', value: 'get_page_version' },
},
{
id: 'propertyKey',
title: 'Property Key',
type: 'short-input',
placeholder: 'Enter property key/name',
required: true,
condition: { field: 'operation', value: 'create_page_property' },
},
{
id: 'propertyValue',
title: 'Property Value',
type: 'long-input',
placeholder: 'Enter property value (JSON supported)',
required: true,
condition: { field: 'operation', value: 'create_page_property' },
},
{
id: 'title',
title: 'Title',
type: 'short-input',
placeholder: 'Enter title for the page',
condition: { field: 'operation', value: ['create', 'update'] },
placeholder: 'Enter title',
condition: { field: 'operation', value: ['create', 'update', 'create_blogpost'] },
},
{
id: 'content',
title: 'Content',
type: 'long-input',
placeholder: 'Enter content for the page',
condition: { field: 'operation', value: ['create', 'update'] },
placeholder: 'Enter content',
condition: { field: 'operation', value: ['create', 'update', 'create_blogpost'] },
},
{
id: 'parentId',
@@ -508,7 +628,7 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
type: 'short-input',
placeholder: 'Enter search query',
required: true,
condition: { field: 'operation', value: 'search' },
condition: { field: 'operation', value: ['search', 'search_in_space'] },
},
{
id: 'comment',
@@ -574,40 +694,140 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
type: 'short-input',
placeholder: 'Enter label name',
required: true,
condition: { field: 'operation', value: ['add_label', 'remove_label'] },
condition: { field: 'operation', value: 'add_label' },
},
{
id: 'labelPrefix',
title: 'Label Prefix',
type: 'dropdown',
options: [
{ label: 'Global (default)', id: 'global' },
{ label: 'My', id: 'my' },
{ label: 'Team', id: 'team' },
{ label: 'System', id: 'system' },
],
value: () => 'global',
condition: { field: 'operation', value: 'add_label' },
},
{
id: 'blogPostStatus',
title: 'Status',
type: 'dropdown',
options: [
{ label: 'Published (current)', id: 'current' },
{ label: 'Draft', id: 'draft' },
],
value: () => 'current',
condition: { field: 'operation', value: 'create_blogpost' },
},
{
id: 'purge',
title: 'Permanently Delete',
type: 'switch',
condition: { field: 'operation', value: 'delete' },
},
{
id: 'bodyFormat',
title: 'Body Format',
type: 'dropdown',
options: [
{ label: 'Storage (default)', id: 'storage' },
{ label: 'Atlas Doc Format', id: 'atlas_doc_format' },
{ label: 'View', id: 'view' },
{ label: 'Export View', id: 'export_view' },
],
value: () => 'storage',
condition: { field: 'operation', value: 'list_comments' },
},
{
id: 'limit',
title: 'Limit',
type: 'short-input',
placeholder: 'Enter maximum number of results (default: 25)',
placeholder: 'Enter maximum number of results (default: 50, max: 250)',
condition: {
field: 'operation',
value: ['search', 'list_comments', 'list_attachments', 'list_spaces'],
value: [
'search',
'search_in_space',
'list_comments',
'list_attachments',
'list_spaces',
'list_pages_in_space',
'list_blogposts',
'list_blogposts_in_space',
'get_page_children',
'list_page_versions',
'list_page_properties',
'list_labels',
],
},
},
{
id: 'cursor',
title: 'Pagination Cursor',
type: 'short-input',
placeholder: 'Enter cursor from previous response (optional)',
condition: {
field: 'operation',
value: [
'list_comments',
'list_attachments',
'list_spaces',
'list_pages_in_space',
'list_blogposts',
'list_blogposts_in_space',
'get_page_children',
'list_page_versions',
'list_page_properties',
'list_labels',
],
},
},
],
tools: {
access: [
// Page Tools
'confluence_retrieve',
'confluence_update',
'confluence_create_page',
'confluence_delete_page',
'confluence_list_pages_in_space',
'confluence_get_page_children',
'confluence_get_page_ancestors',
// Version Tools
'confluence_list_page_versions',
'confluence_get_page_version',
// Property Tools
'confluence_list_page_properties',
'confluence_create_page_property',
// Search Tools
'confluence_search',
'confluence_search_in_space',
// Blog Post Tools
'confluence_list_blogposts',
'confluence_get_blogpost',
'confluence_create_blogpost',
'confluence_list_blogposts_in_space',
// Comment Tools
'confluence_create_comment',
'confluence_list_comments',
'confluence_update_comment',
'confluence_delete_comment',
// Attachment Tools
'confluence_upload_attachment',
'confluence_list_attachments',
'confluence_delete_attachment',
// Label Tools
'confluence_list_labels',
'confluence_add_label',
// Space Tools
'confluence_get_space',
'confluence_list_spaces',
],
config: {
tool: (params) => {
switch (params.operation) {
// Page Operations
case 'read':
return 'confluence_retrieve'
case 'create':
@@ -616,8 +836,37 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
return 'confluence_update'
case 'delete':
return 'confluence_delete_page'
case 'list_pages_in_space':
return 'confluence_list_pages_in_space'
case 'get_page_children':
return 'confluence_get_page_children'
case 'get_page_ancestors':
return 'confluence_get_page_ancestors'
// Version Operations
case 'list_page_versions':
return 'confluence_list_page_versions'
case 'get_page_version':
return 'confluence_get_page_version'
// Property Operations
case 'list_page_properties':
return 'confluence_list_page_properties'
case 'create_page_property':
return 'confluence_create_page_property'
// Search Operations
case 'search':
return 'confluence_search'
case 'search_in_space':
return 'confluence_search_in_space'
// Blog Post Operations
case 'list_blogposts':
return 'confluence_list_blogposts'
case 'get_blogpost':
return 'confluence_get_blogpost'
case 'create_blogpost':
return 'confluence_create_blogpost'
case 'list_blogposts_in_space':
return 'confluence_list_blogposts_in_space'
// Comment Operations
case 'create_comment':
return 'confluence_create_comment'
case 'list_comments':
@@ -626,14 +875,19 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
return 'confluence_update_comment'
case 'delete_comment':
return 'confluence_delete_comment'
// Attachment Operations
case 'upload_attachment':
return 'confluence_upload_attachment'
case 'list_attachments':
return 'confluence_list_attachments'
case 'delete_attachment':
return 'confluence_delete_attachment'
// Label Operations
case 'list_labels':
return 'confluence_list_labels'
case 'add_label':
return 'confluence_add_label'
// Space Operations
case 'get_space':
return 'confluence_get_space'
case 'list_spaces':
@@ -650,11 +904,98 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
attachmentFile,
attachmentFileName,
attachmentComment,
blogPostId,
versionNumber,
propertyKey,
propertyValue,
labelPrefix,
blogPostStatus,
purge,
bodyFormat,
cursor,
...rest
} = params
// Use canonical param (serializer already handles basic/advanced mode)
const effectivePageId = pageId ? String(pageId).trim() : ''
if (operation === 'add_label') {
return {
credential,
pageId: effectivePageId,
operation,
prefix: labelPrefix || 'global',
...rest,
}
}
if (operation === 'create_blogpost') {
return {
credential,
operation,
status: blogPostStatus || 'current',
...rest,
}
}
if (operation === 'delete') {
return {
credential,
pageId: effectivePageId,
operation,
purge: purge || false,
...rest,
}
}
if (operation === 'list_comments') {
return {
credential,
pageId: effectivePageId,
operation,
bodyFormat: bodyFormat || 'storage',
cursor: cursor || undefined,
...rest,
}
}
// Operations that support cursor pagination
const supportsCursor = [
'list_attachments',
'list_spaces',
'list_pages_in_space',
'list_blogposts',
'list_blogposts_in_space',
'get_page_children',
'list_page_versions',
'list_page_properties',
'list_labels',
]
if (supportsCursor.includes(operation) && cursor) {
return {
credential,
pageId: effectivePageId || undefined,
operation,
cursor,
...rest,
}
}
if (operation === 'create_page_property') {
if (!propertyKey) {
throw new Error('Property key is required for this operation.')
}
return {
credential,
pageId: effectivePageId,
operation,
key: propertyKey,
value: propertyValue,
...rest,
}
}
if (operation === 'upload_attachment') {
const normalizedFile = normalizeFileInput(attachmentFile, { single: true })
if (!normalizedFile) {
@@ -674,6 +1015,8 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
return {
credential,
pageId: effectivePageId || undefined,
blogPostId: blogPostId || undefined,
versionNumber: versionNumber ? Number.parseInt(String(versionNumber), 10) : undefined,
operation,
...rest,
}
@@ -686,8 +1029,12 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
credential: { type: 'string', description: 'Confluence access token' },
pageId: { type: 'string', description: 'Page identifier (canonical param)' },
spaceId: { type: 'string', description: 'Space identifier' },
title: { type: 'string', description: 'Page title' },
content: { type: 'string', description: 'Page content' },
blogPostId: { type: 'string', description: 'Blog post identifier' },
versionNumber: { type: 'number', description: 'Page version number' },
propertyKey: { type: 'string', description: 'Property key/name' },
propertyValue: { type: 'json', description: 'Property value (JSON)' },
title: { type: 'string', description: 'Page or blog post title' },
content: { type: 'string', description: 'Page or blog post content' },
parentId: { type: 'string', description: 'Parent page identifier' },
query: { type: 'string', description: 'Search query' },
comment: { type: 'string', description: 'Comment text' },
@@ -697,6 +1044,62 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
attachmentFileName: { type: 'string', description: 'Custom file name for attachment' },
attachmentComment: { type: 'string', description: 'Comment for the attachment' },
labelName: { type: 'string', description: 'Label name' },
labelPrefix: { type: 'string', description: 'Label prefix (global, my, team, system)' },
blogPostStatus: { type: 'string', description: 'Blog post status (current or draft)' },
purge: { type: 'boolean', description: 'Permanently delete instead of moving to trash' },
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' },
},
outputs: {
ts: { type: 'string', description: 'Timestamp' },
pageId: { type: 'string', description: 'Page identifier' },
content: { type: 'string', description: 'Page content' },
body: { type: 'json', description: 'Page body with storage format' },
title: { type: 'string', description: 'Page title' },
url: { type: 'string', description: 'Page or resource URL' },
success: { type: 'boolean', description: 'Operation success status' },
deleted: { type: 'boolean', description: 'Deletion status' },
added: { type: 'boolean', description: 'Addition status' },
removed: { type: 'boolean', description: 'Removal status' },
updated: { type: 'boolean', description: 'Update status' },
// Search & List Results
results: { type: 'array', description: 'Search results' },
pages: { type: 'array', description: 'List of pages' },
children: { type: 'array', description: 'List of child pages' },
ancestors: { type: 'array', description: 'List of ancestor pages' },
// Comment Results
comments: { type: 'array', description: 'List of comments' },
commentId: { type: 'string', description: 'Comment identifier' },
// Attachment Results
attachments: { type: 'array', description: 'List of attachments' },
attachmentId: { type: 'string', description: 'Attachment identifier' },
fileSize: { type: 'number', description: 'Attachment file size in bytes' },
mediaType: { type: 'string', description: 'Attachment MIME type' },
downloadUrl: { type: 'string', description: 'Attachment download URL' },
// Label Results
labels: { type: 'array', description: 'List of labels' },
labelName: { type: 'string', description: 'Label name' },
// Space Results
spaces: { type: 'array', description: 'List of spaces' },
spaceId: { type: 'string', description: 'Space identifier' },
name: { type: 'string', description: 'Space name' },
key: { type: 'string', description: 'Space key' },
type: { type: 'string', description: 'Space or content type' },
status: { type: 'string', description: 'Space status' },
// Blog Post Results
blogPosts: { type: 'array', description: 'List of blog posts' },
blogPostId: { type: 'string', description: 'Blog post identifier' },
// Version Results
versions: { type: 'array', description: 'List of page versions' },
version: { type: 'json', description: 'Version information' },
versionNumber: { type: 'number', description: 'Version number' },
// Property Results
properties: { type: 'array', description: 'List of page properties' },
propertyId: { type: 'string', description: 'Property identifier' },
propertyKey: { type: 'string', description: 'Property key' },
propertyValue: { type: 'json', description: 'Property value' },
// Pagination
nextCursor: { type: 'string', description: 'Cursor for fetching next page of results' },
},
}

View File

@@ -1,7 +1,7 @@
'use client'
import { useEffect } from 'react'
import { getBrandConfig } from '@/lib/branding/branding'
import { getBrandConfig } from '@/ee/whitelabeling'
interface BrandedLayoutProps {
children: React.ReactNode

View File

@@ -1,7 +1,7 @@
import { Section, Text } from '@react-email/components'
import { baseStyles } from '@/components/emails/_styles'
import { EmailLayout } from '@/components/emails/components'
import { getBrandConfig } from '@/lib/branding/branding'
import { getBrandConfig } from '@/ee/whitelabeling'
interface OTPVerificationEmailProps {
otp: string

View File

@@ -1,7 +1,7 @@
import { Link, Text } from '@react-email/components'
import { baseStyles } from '@/components/emails/_styles'
import { EmailLayout } from '@/components/emails/components'
import { getBrandConfig } from '@/lib/branding/branding'
import { getBrandConfig } from '@/ee/whitelabeling'
interface ResetPasswordEmailProps {
username?: string

View File

@@ -1,8 +1,8 @@
import { Link, Text } from '@react-email/components'
import { baseStyles } from '@/components/emails/_styles'
import { EmailLayout } from '@/components/emails/components'
import { getBrandConfig } from '@/lib/branding/branding'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { getBrandConfig } from '@/ee/whitelabeling'
interface WelcomeEmailProps {
userName?: string

View File

@@ -1,8 +1,8 @@
import { Link, Section, Text } from '@react-email/components'
import { baseStyles, colors } from '@/components/emails/_styles'
import { EmailLayout } from '@/components/emails/components'
import { getBrandConfig } from '@/lib/branding/branding'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { getBrandConfig } from '@/ee/whitelabeling'
interface CreditPurchaseEmailProps {
userName?: string

View File

@@ -1,8 +1,8 @@
import { Link, Text } from '@react-email/components'
import { baseStyles } from '@/components/emails/_styles'
import { EmailLayout } from '@/components/emails/components'
import { getBrandConfig } from '@/lib/branding/branding'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { getBrandConfig } from '@/ee/whitelabeling'
interface EnterpriseSubscriptionEmailProps {
userName?: string

View File

@@ -1,7 +1,7 @@
import { Link, Section, Text } from '@react-email/components'
import { baseStyles, colors, typography } from '@/components/emails/_styles'
import { EmailLayout } from '@/components/emails/components'
import { getBrandConfig } from '@/lib/branding/branding'
import { getBrandConfig } from '@/ee/whitelabeling'
interface FreeTierUpgradeEmailProps {
userName?: string

View File

@@ -1,7 +1,7 @@
import { Link, Section, Text } from '@react-email/components'
import { baseStyles, colors } from '@/components/emails/_styles'
import { EmailLayout } from '@/components/emails/components'
import { getBrandConfig } from '@/lib/branding/branding'
import { getBrandConfig } from '@/ee/whitelabeling'
interface PaymentFailedEmailProps {
userName?: string

View File

@@ -1,8 +1,8 @@
import { Link, Text } from '@react-email/components'
import { baseStyles } from '@/components/emails/_styles'
import { EmailLayout } from '@/components/emails/components'
import { getBrandConfig } from '@/lib/branding/branding'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { getBrandConfig } from '@/ee/whitelabeling'
interface PlanWelcomeEmailProps {
planName: 'Pro' | 'Team'

View File

@@ -1,7 +1,7 @@
import { Link, Section, Text } from '@react-email/components'
import { baseStyles } from '@/components/emails/_styles'
import { EmailLayout } from '@/components/emails/components'
import { getBrandConfig } from '@/lib/branding/branding'
import { getBrandConfig } from '@/ee/whitelabeling'
interface UsageThresholdEmailProps {
userName?: string

View File

@@ -2,8 +2,8 @@ import { Text } from '@react-email/components'
import { format } from 'date-fns'
import { baseStyles } from '@/components/emails/_styles'
import { EmailLayout } from '@/components/emails/components'
import { getBrandConfig } from '@/lib/branding/branding'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { getBrandConfig } from '@/ee/whitelabeling'
interface CareersConfirmationEmailProps {
name: string

View File

@@ -1,8 +1,8 @@
import { Container, Img, Link, Section } from '@react-email/components'
import { baseStyles, colors, spacing, typography } from '@/components/emails/_styles'
import { getBrandConfig } from '@/lib/branding/branding'
import { isHosted } from '@/lib/core/config/feature-flags'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { getBrandConfig } from '@/ee/whitelabeling'
interface EmailFooterProps {
baseUrl?: string

View File

@@ -1,8 +1,8 @@
import { Body, Container, Head, Html, Img, Preview, Section } from '@react-email/components'
import { baseStyles } from '@/components/emails/_styles'
import { EmailFooter } from '@/components/emails/components/email-footer'
import { getBrandConfig } from '@/lib/branding/branding'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { getBrandConfig } from '@/ee/whitelabeling'
interface EmailLayoutProps {
/** Preview text shown in email client list view */

View File

@@ -1,7 +1,7 @@
import { Link, Text } from '@react-email/components'
import { baseStyles } from '@/components/emails/_styles'
import { EmailLayout } from '@/components/emails/components'
import { getBrandConfig } from '@/lib/branding/branding'
import { getBrandConfig } from '@/ee/whitelabeling'
interface WorkspaceInvitation {
workspaceId: string

View File

@@ -2,8 +2,8 @@ import { Link, Text } from '@react-email/components'
import { createLogger } from '@sim/logger'
import { baseStyles } from '@/components/emails/_styles'
import { EmailLayout } from '@/components/emails/components'
import { getBrandConfig } from '@/lib/branding/branding'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { getBrandConfig } from '@/ee/whitelabeling'
interface InvitationEmailProps {
inviterName?: string

View File

@@ -1,7 +1,7 @@
import { Link, Text } from '@react-email/components'
import { baseStyles } from '@/components/emails/_styles'
import { EmailLayout } from '@/components/emails/components'
import { getBrandConfig } from '@/lib/branding/branding'
import { getBrandConfig } from '@/ee/whitelabeling'
interface PollingGroupInvitationEmailProps {
inviterName?: string

View File

@@ -2,8 +2,8 @@ import { Link, Text } from '@react-email/components'
import { createLogger } from '@sim/logger'
import { baseStyles } from '@/components/emails/_styles'
import { EmailLayout } from '@/components/emails/components'
import { getBrandConfig } from '@/lib/branding/branding'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { getBrandConfig } from '@/ee/whitelabeling'
const logger = createLogger('WorkspaceInvitationEmail')

View File

@@ -1,7 +1,7 @@
import { Link, Section, Text } from '@react-email/components'
import { baseStyles } from '@/components/emails/_styles'
import { EmailLayout } from '@/components/emails/components'
import { getBrandConfig } from '@/lib/branding/branding'
import { getBrandConfig } from '@/ee/whitelabeling'
/**
* Serialized rate limit status for email payloads.

View File

@@ -1,4 +1,4 @@
import { getBrandConfig } from '@/lib/branding/branding'
import { getBrandConfig } from '@/ee/whitelabeling'
/** Email subject type for all supported email templates */
export type EmailSubjectType =

View File

@@ -27,7 +27,7 @@ under the following terms:
3. ENTERPRISE SUBSCRIPTION
Production deployment of enterprise features requires an active Sim Enterprise
subscription. Contact sales@simstudio.ai for licensing information.
subscription. Contact sales@sim.ai for licensing information.
4. DISCLAIMER
@@ -40,4 +40,4 @@ under the following terms:
IN NO EVENT SHALL SIM STUDIO, INC. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY ARISING FROM THE USE OF THE SOFTWARE.
For questions about enterprise licensing, contact: sales@simstudio.ai
For questions about enterprise licensing, contact: sales@sim.ai

View File

@@ -7,7 +7,7 @@ for production use.
- **SSO (Single Sign-On)**: OIDC and SAML authentication integration
- **Access Control**: Permission groups for fine-grained user access management
- **Credential Sets**: Shared credential pools for email polling workflows
- **Whitelabeling**: Custom branding and theming for enterprise deployments
## Licensing

View File

@@ -0,0 +1,45 @@
import { type BrandConfig, defaultBrandConfig, type ThemeColors } from '@/lib/branding'
import { getEnv } from '@/lib/core/config/env'
export type { BrandConfig, ThemeColors }
const getThemeColors = (): ThemeColors => {
return {
primaryColor:
getEnv('NEXT_PUBLIC_BRAND_PRIMARY_COLOR') || defaultBrandConfig.theme?.primaryColor,
primaryHoverColor:
getEnv('NEXT_PUBLIC_BRAND_PRIMARY_HOVER_COLOR') ||
defaultBrandConfig.theme?.primaryHoverColor,
accentColor: getEnv('NEXT_PUBLIC_BRAND_ACCENT_COLOR') || defaultBrandConfig.theme?.accentColor,
accentHoverColor:
getEnv('NEXT_PUBLIC_BRAND_ACCENT_HOVER_COLOR') || defaultBrandConfig.theme?.accentHoverColor,
backgroundColor:
getEnv('NEXT_PUBLIC_BRAND_BACKGROUND_COLOR') || defaultBrandConfig.theme?.backgroundColor,
}
}
/**
* Get branding configuration from environment variables
* Supports runtime configuration via Docker/Kubernetes
*/
export const getBrandConfig = (): BrandConfig => {
return {
name: getEnv('NEXT_PUBLIC_BRAND_NAME') || defaultBrandConfig.name,
logoUrl: getEnv('NEXT_PUBLIC_BRAND_LOGO_URL') || defaultBrandConfig.logoUrl,
faviconUrl: getEnv('NEXT_PUBLIC_BRAND_FAVICON_URL') || defaultBrandConfig.faviconUrl,
customCssUrl: getEnv('NEXT_PUBLIC_CUSTOM_CSS_URL') || defaultBrandConfig.customCssUrl,
supportEmail: getEnv('NEXT_PUBLIC_SUPPORT_EMAIL') || defaultBrandConfig.supportEmail,
documentationUrl:
getEnv('NEXT_PUBLIC_DOCUMENTATION_URL') || defaultBrandConfig.documentationUrl,
termsUrl: getEnv('NEXT_PUBLIC_TERMS_URL') || defaultBrandConfig.termsUrl,
privacyUrl: getEnv('NEXT_PUBLIC_PRIVACY_URL') || defaultBrandConfig.privacyUrl,
theme: getThemeColors(),
}
}
/**
* Hook to use brand configuration in React components
*/
export const useBrandConfig = () => {
return getBrandConfig()
}

View File

@@ -0,0 +1,4 @@
export type { BrandConfig, ThemeColors } from './branding'
export { getBrandConfig, useBrandConfig } from './branding'
export { generateThemeCSS } from './inject-theme'
export { generateBrandedMetadata, generateStructuredData } from './metadata'

View File

@@ -1,4 +1,6 @@
// Helper to detect if background is dark
/**
* Helper to detect if background is dark
*/
function isDarkBackground(hexColor: string): boolean {
const hex = hexColor.replace('#', '')
const r = Number.parseInt(hex.substr(0, 2), 16)

View File

@@ -1,6 +1,6 @@
import type { Metadata } from 'next'
import { getBrandConfig } from '@/lib/branding/branding'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { getBrandConfig } from '@/ee/whitelabeling/branding'
/**
* Generate dynamic metadata based on brand configuration

View File

@@ -151,7 +151,8 @@ export const auth = betterAuth({
create: {
before: async (account) => {
// Only one credential per (userId, providerId) is allowed
// If user reconnects (even with a different external account), replace the existing one
// If user reconnects (even with a different external account), delete the old one
// and let Better Auth create the new one (returning false breaks account linking flow)
const existing = await db.query.account.findFirst({
where: and(
eq(schema.account.userId, account.userId),
@@ -159,101 +160,59 @@ export const auth = betterAuth({
),
})
if (existing) {
let scopeToStore = account.scope
const modifiedAccount = { ...account }
if (account.providerId === 'salesforce' && account.accessToken) {
try {
const response = await fetch(
'https://login.salesforce.com/services/oauth2/userinfo',
{
headers: {
Authorization: `Bearer ${account.accessToken}`,
},
}
)
if (response.ok) {
const data = await response.json()
if (data.profile) {
const match = data.profile.match(/^(https:\/\/[^/]+)/)
if (match && match[1] !== 'https://login.salesforce.com') {
const instanceUrl = match[1]
scopeToStore = `__sf_instance__:${instanceUrl} ${account.scope}`
}
}
if (account.providerId === 'salesforce' && account.accessToken) {
try {
const response = await fetch(
'https://login.salesforce.com/services/oauth2/userinfo',
{
headers: {
Authorization: `Bearer ${account.accessToken}`,
},
}
} catch (error) {
logger.error('Failed to fetch Salesforce instance URL', { error })
}
}
const refreshTokenExpiresAt = isMicrosoftProvider(account.providerId)
? getMicrosoftRefreshTokenExpiry()
: account.refreshTokenExpiresAt
await db
.update(schema.account)
.set({
accountId: account.accountId,
accessToken: account.accessToken,
refreshToken: account.refreshToken,
idToken: account.idToken,
accessTokenExpiresAt: account.accessTokenExpiresAt,
refreshTokenExpiresAt,
scope: scopeToStore,
updatedAt: new Date(),
})
.where(eq(schema.account.id, existing.id))
// Sync webhooks for credential sets after reconnecting
const requestId = crypto.randomUUID().slice(0, 8)
const userMemberships = await db
.select({
credentialSetId: schema.credentialSetMember.credentialSetId,
providerId: schema.credentialSet.providerId,
})
.from(schema.credentialSetMember)
.innerJoin(
schema.credentialSet,
eq(schema.credentialSetMember.credentialSetId, schema.credentialSet.id)
)
.where(
and(
eq(schema.credentialSetMember.userId, account.userId),
eq(schema.credentialSetMember.status, 'active')
)
)
for (const membership of userMemberships) {
if (membership.providerId === account.providerId) {
try {
await syncAllWebhooksForCredentialSet(membership.credentialSetId, requestId)
logger.info(
'[account.create.before] Synced webhooks after credential reconnect',
{
credentialSetId: membership.credentialSetId,
providerId: account.providerId,
}
)
} catch (error) {
logger.error(
'[account.create.before] Failed to sync webhooks after credential reconnect',
{
credentialSetId: membership.credentialSetId,
providerId: account.providerId,
error,
}
)
if (response.ok) {
const data = await response.json()
if (data.profile) {
const match = data.profile.match(/^(https:\/\/[^/]+)/)
if (match && match[1] !== 'https://login.salesforce.com') {
const instanceUrl = match[1]
modifiedAccount.scope = `__sf_instance__:${instanceUrl} ${account.scope}`
}
}
}
} catch (error) {
logger.error('Failed to fetch Salesforce instance URL', { error })
}
return false
}
return { data: account }
// Handle Microsoft refresh token expiry
if (isMicrosoftProvider(account.providerId)) {
modifiedAccount.refreshTokenExpiresAt = getMicrosoftRefreshTokenExpiry()
}
if (existing) {
// Delete the existing account so Better Auth can create the new one
// This allows account linking/re-authorization to succeed
await db.delete(schema.account).where(eq(schema.account.id, existing.id))
// Preserve the existing account ID so references (like workspace notifications) continue to work
modifiedAccount.id = existing.id
logger.info('[account.create.before] Deleted existing account for re-authorization', {
userId: account.userId,
providerId: account.providerId,
existingAccountId: existing.id,
preservingId: true,
})
// Sync webhooks for credential sets after reconnecting (in after hook)
}
return { data: modifiedAccount }
},
after: async (account) => {
try {
@@ -1687,6 +1646,12 @@ export const auth = betterAuth({
'search:confluence',
'read:me',
'offline_access',
'read:blogpost:confluence',
'write:blogpost:confluence',
'read:content.property:confluence',
'write:content.property:confluence',
'read:hierarchical-content:confluence',
'read:content.metadata:confluence',
],
responseType: 'code',
pkce: true,

View File

@@ -1,80 +0,0 @@
import { getEnv } from '@/lib/core/config/env'
export interface ThemeColors {
primaryColor?: string
primaryHoverColor?: string
accentColor?: string
accentHoverColor?: string
backgroundColor?: string
}
export interface BrandConfig {
name: string
logoUrl?: string
faviconUrl?: string
customCssUrl?: string
supportEmail?: string
documentationUrl?: string
termsUrl?: string
privacyUrl?: string
theme?: ThemeColors
}
/**
* Default brand configuration values
*/
const defaultConfig: BrandConfig = {
name: 'Sim',
logoUrl: undefined,
faviconUrl: '/favicon/favicon.ico',
customCssUrl: undefined,
supportEmail: 'help@sim.ai',
documentationUrl: undefined,
termsUrl: undefined,
privacyUrl: undefined,
theme: {
primaryColor: '#701ffc',
primaryHoverColor: '#802fff',
accentColor: '#9d54ff',
accentHoverColor: '#a66fff',
backgroundColor: '#0c0c0c',
},
}
const getThemeColors = (): ThemeColors => {
return {
primaryColor: getEnv('NEXT_PUBLIC_BRAND_PRIMARY_COLOR') || defaultConfig.theme?.primaryColor,
primaryHoverColor:
getEnv('NEXT_PUBLIC_BRAND_PRIMARY_HOVER_COLOR') || defaultConfig.theme?.primaryHoverColor,
accentColor: getEnv('NEXT_PUBLIC_BRAND_ACCENT_COLOR') || defaultConfig.theme?.accentColor,
accentHoverColor:
getEnv('NEXT_PUBLIC_BRAND_ACCENT_HOVER_COLOR') || defaultConfig.theme?.accentHoverColor,
backgroundColor:
getEnv('NEXT_PUBLIC_BRAND_BACKGROUND_COLOR') || defaultConfig.theme?.backgroundColor,
}
}
/**
* Get branding configuration from environment variables
* Supports runtime configuration via Docker/Kubernetes
*/
export const getBrandConfig = (): BrandConfig => {
return {
name: getEnv('NEXT_PUBLIC_BRAND_NAME') || defaultConfig.name,
logoUrl: getEnv('NEXT_PUBLIC_BRAND_LOGO_URL') || defaultConfig.logoUrl,
faviconUrl: getEnv('NEXT_PUBLIC_BRAND_FAVICON_URL') || defaultConfig.faviconUrl,
customCssUrl: getEnv('NEXT_PUBLIC_CUSTOM_CSS_URL') || defaultConfig.customCssUrl,
supportEmail: getEnv('NEXT_PUBLIC_SUPPORT_EMAIL') || defaultConfig.supportEmail,
documentationUrl: getEnv('NEXT_PUBLIC_DOCUMENTATION_URL') || defaultConfig.documentationUrl,
termsUrl: getEnv('NEXT_PUBLIC_TERMS_URL') || defaultConfig.termsUrl,
privacyUrl: getEnv('NEXT_PUBLIC_PRIVACY_URL') || defaultConfig.privacyUrl,
theme: getThemeColors(),
}
}
/**
* Hook to use brand configuration in React components
*/
export const useBrandConfig = () => {
return getBrandConfig()
}

Some files were not shown because too many files have changed in this diff Show More