Skip to content

Commit 3d6660b

Browse files
waleedlatif1claude
andauthored
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>
1 parent 48e174b commit 3d6660b

File tree

5 files changed

+57
-66
lines changed

5 files changed

+57
-66
lines changed

apps/sim/app/api/tools/jira/update/route.ts

Lines changed: 5 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
33
import { z } from 'zod'
44
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
55
import { validateJiraCloudId, validateJiraIssueKey } from '@/lib/core/security/input-validation'
6-
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
6+
import { getJiraCloudId, parseAtlassianErrorMessage, toAdf } from '@/tools/jira/utils'
77

88
export const dynamic = 'force-dynamic'
99

@@ -15,14 +15,14 @@ const jiraUpdateSchema = z.object({
1515
issueKey: z.string().min(1, 'Issue key is required'),
1616
summary: z.string().optional(),
1717
title: z.string().optional(),
18-
description: z.string().optional(),
18+
description: z.union([z.string(), z.record(z.unknown())]).optional(),
1919
priority: z.string().optional(),
2020
assignee: z.string().optional(),
2121
labels: z.array(z.string()).optional(),
2222
components: z.array(z.string()).optional(),
2323
duedate: z.string().optional(),
2424
fixVersions: z.array(z.string()).optional(),
25-
environment: z.string().optional(),
25+
environment: z.union([z.string(), z.record(z.unknown())]).optional(),
2626
customFieldId: z.string().optional(),
2727
customFieldValue: z.string().optional(),
2828
notifyUsers: z.boolean().optional(),
@@ -91,21 +91,7 @@ export async function PUT(request: NextRequest) {
9191
}
9292

9393
if (description !== undefined && description !== null && description !== '') {
94-
fields.description = {
95-
type: 'doc',
96-
version: 1,
97-
content: [
98-
{
99-
type: 'paragraph',
100-
content: [
101-
{
102-
type: 'text',
103-
text: description,
104-
},
105-
],
106-
},
107-
],
108-
}
94+
fields.description = toAdf(description)
10995
}
11096

11197
if (priority !== undefined && priority !== null && priority !== '') {
@@ -136,21 +122,7 @@ export async function PUT(request: NextRequest) {
136122
}
137123

138124
if (environment !== undefined && environment !== null && environment !== '') {
139-
fields.environment = {
140-
type: 'doc',
141-
version: 1,
142-
content: [
143-
{
144-
type: 'paragraph',
145-
content: [
146-
{
147-
type: 'text',
148-
text: environment,
149-
},
150-
],
151-
},
152-
],
153-
}
125+
fields.environment = toAdf(environment)
154126
}
155127

156128
if (

apps/sim/app/api/tools/jira/write/route.ts

Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
44
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
5-
import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils'
5+
import { getJiraCloudId, parseAtlassianErrorMessage, toAdf } from '@/tools/jira/utils'
66

77
export const dynamic = 'force-dynamic'
88

@@ -85,21 +85,7 @@ export async function POST(request: NextRequest) {
8585
}
8686

8787
if (description !== undefined && description !== null && description !== '') {
88-
fields.description = {
89-
type: 'doc',
90-
version: 1,
91-
content: [
92-
{
93-
type: 'paragraph',
94-
content: [
95-
{
96-
type: 'text',
97-
text: description,
98-
},
99-
],
100-
},
101-
],
102-
}
88+
fields.description = toAdf(description)
10389
}
10490

10591
if (parent !== undefined && parent !== null && parent !== '') {
@@ -144,21 +130,7 @@ export async function POST(request: NextRequest) {
144130
}
145131

146132
if (environment !== undefined && environment !== null && environment !== '') {
147-
fields.environment = {
148-
type: 'doc',
149-
version: 1,
150-
content: [
151-
{
152-
type: 'paragraph',
153-
content: [
154-
{
155-
type: 'text',
156-
text: environment,
157-
},
158-
],
159-
},
160-
],
161-
}
133+
fields.environment = toAdf(environment)
162134
}
163135

164136
if (

apps/sim/tools/jira/update.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ export const jiraUpdateTool: ToolConfig<JiraUpdateParams, JiraUpdateResponse> =
4242
type: 'string',
4343
required: false,
4444
visibility: 'user-or-llm',
45-
description: 'New description for the issue',
45+
description:
46+
'New description for the issue. Accepts plain text (auto-wrapped in ADF) or a raw ADF document object',
4647
},
4748
priority: {
4849
type: 'string',

apps/sim/tools/jira/utils.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,51 @@ const logger = createLogger('JiraUtils')
55

66
const MAX_ATTACHMENT_SIZE = 50 * 1024 * 1024
77

8+
/**
9+
* Converts a value to ADF format. If the value is already an ADF document object,
10+
* it is returned as-is. If it is a plain string, it is wrapped in a single-paragraph ADF doc.
11+
*/
12+
export function toAdf(value: string | Record<string, unknown>): Record<string, unknown> {
13+
if (typeof value === 'object') {
14+
if (value.type === 'doc') {
15+
return value
16+
}
17+
if (value.type && Array.isArray(value.content)) {
18+
return { type: 'doc', version: 1, content: [value] }
19+
}
20+
}
21+
if (typeof value === 'string') {
22+
try {
23+
const parsed = JSON.parse(value)
24+
if (typeof parsed === 'object' && parsed !== null && parsed.type === 'doc') {
25+
return parsed
26+
}
27+
if (
28+
typeof parsed === 'object' &&
29+
parsed !== null &&
30+
parsed.type &&
31+
Array.isArray(parsed.content)
32+
) {
33+
return { type: 'doc', version: 1, content: [parsed] }
34+
}
35+
} catch {
36+
// Not JSON — treat as plain text below
37+
}
38+
}
39+
return {
40+
type: 'doc',
41+
version: 1,
42+
content: [
43+
{
44+
type: 'paragraph',
45+
content: [
46+
{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value) },
47+
],
48+
},
49+
],
50+
}
51+
}
52+
853
/**
954
* Extracts plain text from Atlassian Document Format (ADF) content.
1055
* Returns null if content is falsy.

apps/sim/tools/jira/write.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ export const jiraWriteTool: ToolConfig<JiraWriteParams, JiraWriteResponse> = {
4242
type: 'string',
4343
required: false,
4444
visibility: 'user-or-llm',
45-
description: 'Description for the issue',
45+
description:
46+
'Description for the issue. Accepts plain text (auto-wrapped in ADF) or a raw ADF document object',
4647
},
4748
priority: {
4849
type: 'string',

0 commit comments

Comments
 (0)