From 3d6660ba4d633790dffc8cecb0fc863e334e5539 Mon Sep 17 00:00:00 2001 From: Waleed Date: Tue, 14 Apr 2026 15:18:18 -0700 Subject: [PATCH] feat(jira): support raw ADF in description and environment fields (#4164) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(security): resolve ReDoS vulnerability in function execute tag pattern Simplified regex to eliminate overlapping quantifiers that caused exponential backtracking on malformed input without closing delimiter. * feat(jira): support raw ADF document objects in description and environment fields Add toAdf() helper that passes through ADF objects as-is or wraps plain text in a single-paragraph ADF doc. Update write and update routes to use it, replacing inline ADF wrapping. Update Zod schema to accept string or object for description. Fully backward compatible — plain text still works, but callers can now pass rich ADF with expand nodes, tables, code blocks, etc. Co-Authored-By: Claude Opus 4.6 * fix(jira): handle partial ADF nodes and non-ADF objects in toAdf() Wrap partial ADF nodes (type + content but not doc) in a doc envelope. Fall back to JSON.stringify for non-ADF objects instead of String() which produces [object Object]. Co-Authored-By: Claude Opus 4.6 * lint * fix(jira): handle JSON-stringified ADF in toAdf() for variable resolution The executor's formatValueForBlock() JSON.stringify's object values when resolving references. This means an ADF object from an upstream Agent block arrives at the route as a JSON string. toAdf() now detects JSON strings containing valid ADF documents or nodes and parses them back, ensuring rich formatting is preserved through the pipeline. Co-Authored-By: Claude Opus 4.6 * lint changes * fix(jira): update environment Zod schema to accept ADF objects Match the description field schema change — environment also passes through toAdf() so its Zod schema must accept objects too. Co-Authored-By: Claude Opus 4.6 * updated lobkc --------- Co-authored-by: Claude Opus 4.6 --- apps/sim/app/api/tools/jira/update/route.ts | 38 +++-------------- apps/sim/app/api/tools/jira/write/route.ts | 34 ++-------------- apps/sim/tools/jira/update.ts | 3 +- apps/sim/tools/jira/utils.ts | 45 +++++++++++++++++++++ apps/sim/tools/jira/write.ts | 3 +- 5 files changed, 57 insertions(+), 66 deletions(-) diff --git a/apps/sim/app/api/tools/jira/update/route.ts b/apps/sim/app/api/tools/jira/update/route.ts index 8ad96ba3d0..2c0f5dcb4a 100644 --- a/apps/sim/app/api/tools/jira/update/route.ts +++ b/apps/sim/app/api/tools/jira/update/route.ts @@ -3,7 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation' -import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' +import { getJiraCloudId, parseAtlassianErrorMessage, toAdf } from '@/tools/jira/utils' export const dynamic = 'force-dynamic' @@ -15,14 +15,14 @@ const jiraUpdateSchema = z.object({ issueKey: z.string().min(1, 'Issue key is required'), summary: z.string().optional(), title: z.string().optional(), - description: z.string().optional(), + description: z.union([z.string(), z.record(z.unknown())]).optional(), priority: z.string().optional(), assignee: z.string().optional(), labels: z.array(z.string()).optional(), components: z.array(z.string()).optional(), duedate: z.string().optional(), fixVersions: z.array(z.string()).optional(), - environment: z.string().optional(), + environment: z.union([z.string(), z.record(z.unknown())]).optional(), customFieldId: z.string().optional(), customFieldValue: z.string().optional(), notifyUsers: z.boolean().optional(), @@ -91,21 +91,7 @@ export async function PUT(request: NextRequest) { } if (description !== undefined && description !== null && description !== '') { - fields.description = { - type: 'doc', - version: 1, - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: description, - }, - ], - }, - ], - } + fields.description = toAdf(description) } if (priority !== undefined && priority !== null && priority !== '') { @@ -136,21 +122,7 @@ export async function PUT(request: NextRequest) { } if (environment !== undefined && environment !== null && environment !== '') { - fields.environment = { - type: 'doc', - version: 1, - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: environment, - }, - ], - }, - ], - } + fields.environment = toAdf(environment) } if ( diff --git a/apps/sim/app/api/tools/jira/write/route.ts b/apps/sim/app/api/tools/jira/write/route.ts index 6ecb49553c..d63689b267 100644 --- a/apps/sim/app/api/tools/jira/write/route.ts +++ b/apps/sim/app/api/tools/jira/write/route.ts @@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' -import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' +import { getJiraCloudId, parseAtlassianErrorMessage, toAdf } from '@/tools/jira/utils' export const dynamic = 'force-dynamic' @@ -85,21 +85,7 @@ export async function POST(request: NextRequest) { } if (description !== undefined && description !== null && description !== '') { - fields.description = { - type: 'doc', - version: 1, - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: description, - }, - ], - }, - ], - } + fields.description = toAdf(description) } if (parent !== undefined && parent !== null && parent !== '') { @@ -144,21 +130,7 @@ export async function POST(request: NextRequest) { } if (environment !== undefined && environment !== null && environment !== '') { - fields.environment = { - type: 'doc', - version: 1, - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: environment, - }, - ], - }, - ], - } + fields.environment = toAdf(environment) } if ( diff --git a/apps/sim/tools/jira/update.ts b/apps/sim/tools/jira/update.ts index 75f53fc876..47e8e26693 100644 --- a/apps/sim/tools/jira/update.ts +++ b/apps/sim/tools/jira/update.ts @@ -42,7 +42,8 @@ export const jiraUpdateTool: ToolConfig = type: 'string', required: false, visibility: 'user-or-llm', - description: 'New description for the issue', + description: + 'New description for the issue. Accepts plain text (auto-wrapped in ADF) or a raw ADF document object', }, priority: { type: 'string', diff --git a/apps/sim/tools/jira/utils.ts b/apps/sim/tools/jira/utils.ts index d29f9c792b..02f5a28b0b 100644 --- a/apps/sim/tools/jira/utils.ts +++ b/apps/sim/tools/jira/utils.ts @@ -5,6 +5,51 @@ const logger = createLogger('JiraUtils') const MAX_ATTACHMENT_SIZE = 50 * 1024 * 1024 +/** + * Converts a value to ADF format. If the value is already an ADF document object, + * it is returned as-is. If it is a plain string, it is wrapped in a single-paragraph ADF doc. + */ +export function toAdf(value: string | Record): Record { + if (typeof value === 'object') { + if (value.type === 'doc') { + return value + } + if (value.type && Array.isArray(value.content)) { + return { type: 'doc', version: 1, content: [value] } + } + } + if (typeof value === 'string') { + try { + const parsed = JSON.parse(value) + if (typeof parsed === 'object' && parsed !== null && parsed.type === 'doc') { + return parsed + } + if ( + typeof parsed === 'object' && + parsed !== null && + parsed.type && + Array.isArray(parsed.content) + ) { + return { type: 'doc', version: 1, content: [parsed] } + } + } catch { + // Not JSON — treat as plain text below + } + } + return { + type: 'doc', + version: 1, + content: [ + { + type: 'paragraph', + content: [ + { type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value) }, + ], + }, + ], + } +} + /** * Extracts plain text from Atlassian Document Format (ADF) content. * Returns null if content is falsy. diff --git a/apps/sim/tools/jira/write.ts b/apps/sim/tools/jira/write.ts index 42a5f9391c..db1fac87f8 100644 --- a/apps/sim/tools/jira/write.ts +++ b/apps/sim/tools/jira/write.ts @@ -42,7 +42,8 @@ export const jiraWriteTool: ToolConfig = { type: 'string', required: false, visibility: 'user-or-llm', - description: 'Description for the issue', + description: + 'Description for the issue. Accepts plain text (auto-wrapped in ADF) or a raw ADF document object', }, priority: { type: 'string',