feat(jira): support raw ADF in description and environment fields (#4164)

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* lint

* fix(jira): handle JSON-stringified ADF in toAdf() for variable resolution

The executor's formatValueForBlock() JSON.stringify's object values when
resolving <Block.output> 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* updated lobkc

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Waleed
2026-04-14 15:18:18 -07:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 48e174b21f
commit 3d6660ba4d
5 changed files with 57 additions and 66 deletions
+5 -33
View File
@@ -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 (
+3 -31
View File
@@ -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 (
+2 -1
View File
@@ -42,7 +42,8 @@ export const jiraUpdateTool: ToolConfig<JiraUpdateParams, JiraUpdateResponse> =
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',
+45
View File
@@ -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<string, unknown>): Record<string, unknown> {
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.
+2 -1
View File
@@ -42,7 +42,8 @@ export const jiraWriteTool: ToolConfig<JiraWriteParams, JiraWriteResponse> = {
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',