Skip to content

Commit b266bd9

Browse files
authored
fix(integrations): final review pass for the Atlassian and AWS sweeps (#9961)
* fix(atlassian): address final review Make Jira search heavy fields (ADF description, custom fields, attachments, links, subtasks) opt-in through fields, move transformIssueData into jira/utils, keep AQL order by splitting at parenthesis depth 0, let the object-types /flat fallback never add a failure, send next=true with Confluence search cursors, and harden Trello ID lists, Jira parent, and Confluence comment ids for agent calls. * fix(aws): address final review Share the optional-boolean parse through @sim/utils/coerce toBooleanFromInput, the IAM tags schema through iam-shared, and the SES message-tag schema through ses-shared. Accept SES email tags as a JSON string from agent calls, name the field on Secrets Manager JSON errors, accept JSON-array IAM tag strings and skip null tag values, and reject a JSON-object Textract query string. * fix(integrations): address review on the final pass Treat only a leading [ as a JSON Textract query list, reject JSON-array IAM tag strings and SES email-tag strings that do not parse to valid entries, name Secrets Manager tag JSON errors, restore the JSM list TSDoc placement, and shrink the explicit-any baseline after the Jira mapper move. * fix(integrations): reject JSON-object Textract queries only when they parse, keep null IAM tag values as empty * fix(textract): use isRecordLike for the JSON-object query check
1 parent 8f2bd0b commit b266bd9

43 files changed

Lines changed: 917 additions & 376 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/docs/content/docs/integrations/jira.mdx‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -548,7 +548,7 @@ Search for Jira issues using JQL (Jira Query Language)
548548
| `jql` | string | Yes | JQL query string to search for issues \(e.g., "project = PROJ AND status = Open"\) |
549549
| `nextPageToken` | string | No | Cursor token for the next page of results. Omit for the first page. |
550550
| `maxResults` | number | No | Maximum number of results to return per page \(default: 50\) |
551-
| `fields` | array | No | Array of field names to return \(default: all fields\). |
551+
| `fields` | array | No | Array of field names to return \(default: all fields\). When set, each issue also includes descriptionAdf, customFields, attachments, issuelinks, and subtasks. |
552552
| `expand` | string | No | Comma-separated expansions: renderedFields, names, schema, transitions, operations, editmeta, changelog |
553553

554554
#### Output
@@ -562,8 +562,8 @@ Search for Jira issues using JQL (Jira Query Language)
562562
| ↳ `self` | string | REST API URL for this issue |
563563
| ↳ `summary` | string | Issue summary |
564564
| ↳ `description` | string | Issue description text \(extracted from ADF\) |
565-
| ↳ `descriptionAdf` | json | Issue description as the raw Atlassian Document Format \(ADF\) document |
566-
| ↳ `renderedDescription` | string | Issue description rendered as HTML \(requires the renderedFields expansion\) |
565+
| ↳ `descriptionAdf` | json | Issue description as the raw Atlassian Document Format \(ADF\) document. Only included when the fields param is set |
566+
| ↳ `renderedDescription` | string | Issue description rendered as HTML. Only included when expand includes renderedFields |
567567
| ↳ `environment` | string | Environment text \(extracted from ADF\) |
568568
| ↳ `status` | object | Issue status |
569569
| ↳ `id` | string | Status ID |
@@ -644,7 +644,7 @@ Search for Jira issues using JQL (Jira Query Language)
644644
| ↳ `id` | string | Parent issue ID |
645645
| ↳ `key` | string | Parent issue key |
646646
| ↳ `summary` | string | Parent issue summary |
647-
| ↳ `issuelinks` | array | Linked issues |
647+
| ↳ `issuelinks` | array | Linked issues. Only included when the fields param is set |
648648
| ↳ `id` | string | Issue link ID |
649649
| ↳ `type` | object | Link type information |
650650
| ↳ `id` | string | Link type ID |
@@ -661,7 +661,7 @@ Search for Jira issues using JQL (Jira Query Language)
661661
| ↳ `key` | string | Issue key |
662662
| ↳ `statusName` | string | Issue status name |
663663
| ↳ `summary` | string | Issue summary |
664-
| ↳ `subtasks` | array | Subtask issues |
664+
| ↳ `subtasks` | array | Subtask issues. Only included when the fields param is set |
665665
| ↳ `id` | string | Subtask issue ID |
666666
| ↳ `key` | string | Subtask issue key |
667667
| ↳ `summary` | string | Subtask summary |
@@ -673,7 +673,7 @@ Search for Jira issues using JQL (Jira Query Language)
673673
| ↳ `watches` | object | Watch information |
674674
| ↳ `watchCount` | number | Number of watchers |
675675
| ↳ `isWatching` | boolean | Whether the current user is watching |
676-
| ↳ `attachments` | array | Attachment metadata \(no file contents\) |
676+
| ↳ `attachments` | array | Attachment metadata \(no file contents\). Only included when the fields param is set |
677677
| ↳ `id` | string | Attachment ID |
678678
| ↳ `filename` | string | Attachment file name |
679679
| ↳ `mimeType` | string | MIME type of the attachment |
@@ -690,7 +690,7 @@ Search for Jira issues using JQL (Jira Query Language)
690690
| ↳ `timeZone` | string | User timezone |
691691
| ↳ `authorName` | string | Attachment author display name |
692692
| ↳ `created` | string | ISO 8601 timestamp when the attachment was created |
693-
| ↳ `customFields` | json | Every customfield_* value Jira returned, keyed by field ID \(e.g., sprint, story points, team\). Use the names output or Get Fields to map IDs to names. |
693+
| ↳ `customFields` | json | Every customfield_* value Jira returned, keyed by field ID \(e.g., sprint, story points, team\). Only included when the fields param is set; use the names output or Get Fields to map IDs to names. |
694694
| `nextPageToken` | string | Cursor token for the next page. Null when no more results. |
695695
| `isLast` | boolean | Whether this is the last page of results |
696696
| `names` | json | Map of field ID to display name \(e.g., customfield_10020 → "Sprint"\). Only returned when expand includes names. |

‎apps/docs/content/docs/integrations/trello.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -363,8 +363,8 @@ Update an existing card on Trello
363363
| `start` | string | No | Start date \(ISO 8601 format\) |
364364
| `pos` | string | No | Position of the card in its list \(top, bottom, or positive float\) |
365365
| `idBoard` | string | No | Trello board ID to move the card to \(use with idList\) |
366-
| `labelIds` | array | No | Replace the card labels with these label IDs. An empty list leaves labels unchanged; use Remove Label to take one off |
367-
| `memberIds` | array | No | Replace the card members with these member IDs. An empty list leaves members unchanged; use Remove Member to take one off |
366+
| `labelIds` | array | No | Replace the card labels with these label IDs \(array or comma-separated string\). An empty list leaves labels unchanged; use Remove Label to take one off |
367+
| `memberIds` | array | No | Replace the card members with these member IDs \(array or comma-separated string\). An empty list leaves members unchanged; use Remove Member to take one off |
368368

369369
#### Output
370370

‎apps/sim/blocks/blocks/aws-stale-inputs.test.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,15 @@ describe('AWS blocks keep stale subBlock values out of tool input', () => {
9393
expect(body.policyUsageFilter).toBeUndefined()
9494
expect(awsIamListPoliciesContract.body?.safeParse(body).success).toBe(true)
9595
})
96+
97+
it('create user rejects a JSON-array tags string with no valid entries', () => {
98+
expect(() =>
99+
executorParams(IAMBlock, {
100+
...AWS,
101+
operation: 'create_user',
102+
userName: 'alice',
103+
tags: '[{"name":"team","val":"core"}]',
104+
})
105+
).toThrow('Tags must be a list of {"key", "value"} entries or a JSON object')
106+
})
96107
})

‎apps/sim/blocks/blocks/iam.ts‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,34 @@ import { AuthMode, IntegrationType } from '@/blocks/types'
55
import { withSavedCredential } from '@/blocks/utils'
66

77
/**
8-
* Converts the Tags table, a `[{ key, value }]` list, a plain object, or a JSON object
9-
* string into the JSON object string the tools accept.
8+
* Converts the Tags table, a `[{ key, value }]` list (or its JSON string), a plain
9+
* object, or a JSON object string into the JSON object string the tools accept.
1010
*/
1111
function serializeTags(tags: unknown): string | undefined {
12-
if (typeof tags === 'string') return tags.trim() ? tags : undefined
12+
if (typeof tags === 'string') {
13+
const trimmed = tags.trim()
14+
if (!trimmed) return undefined
15+
if (!trimmed.startsWith('[')) return trimmed
16+
let parsed: unknown
17+
try {
18+
parsed = JSON.parse(trimmed)
19+
} catch {
20+
return trimmed
21+
}
22+
const serialized = serializeTags(parsed)
23+
if (serialized === undefined && Array.isArray(parsed) && parsed.length > 0) {
24+
throw new Error('Tags must be a list of {"key", "value"} entries or a JSON object')
25+
}
26+
return serialized
27+
}
1328
if (isRecordLike(tags)) return Object.keys(tags).length > 0 ? JSON.stringify(tags) : undefined
1429
if (!Array.isArray(tags)) return undefined
1530
const obj: Record<string, string> = {}
1631
for (const row of tags) {
1732
const entry = isRecordLike(row) && isRecordLike(row.cells) ? row.cells : row
1833
const key = isRecordLike(entry) ? entry.key : undefined
1934
const value = isRecordLike(entry) ? entry.value : undefined
20-
if (typeof key === 'string' && key && value !== undefined) obj[key] = String(value)
35+
if (typeof key === 'string' && key && value !== undefined) obj[key] = String(value ?? '')
2136
}
2237
return Object.keys(obj).length > 0 ? JSON.stringify(obj) : undefined
2338
}

‎apps/sim/blocks/blocks/jira.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1619,7 +1619,7 @@ Return ONLY the comment text - no explanations.`,
16191619
issues: {
16201620
type: 'json',
16211621
description:
1622-
'Array of matching issues with key, summary, description, descriptionAdf, status, assignee, reporter, priority, labels, components, fixVersions, parent, subtasks, issuelinks, attachments, timetracking, customFields, dates',
1622+
'Array of matching issues with key, summary, description, status, assignee, reporter, priority, labels, components, fixVersions, parent, timetracking, dates. descriptionAdf, subtasks, issuelinks, attachments, and customFields are only included when Fields is set; renderedDescription needs the renderedFields expansion',
16231623
},
16241624

16251625
comments: {

‎apps/sim/blocks/blocks/secrets_manager.ts‎

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
1+
import { getErrorMessage } from '@sim/utils/errors'
12
import { SecretsManagerIcon } from '@/components/icons'
23
import type { BlockConfig, BlockMeta } from '@/blocks/types'
34
import { AuthMode, IntegrationType } from '@/blocks/types'
45
import { withSavedCredential } from '@/blocks/utils'
56

7+
/** Parses a JSON field typed into the block, naming the field when it is invalid. */
8+
function parseJsonField(value: unknown, fieldName: string): unknown {
9+
if (typeof value !== 'string') return value
10+
try {
11+
return JSON.parse(value)
12+
} catch (error) {
13+
throw new Error(`Invalid JSON in ${fieldName}: ${getErrorMessage(error)}`)
14+
}
15+
}
16+
617
export const SecretsManagerBlock: BlockConfig = {
718
type: 'secrets_manager',
819
hideFromToolbar: true,
@@ -433,10 +444,7 @@ Return ONLY the JSON array - no explanations, no markdown code blocks.`,
433444
}
434445
if (rest.nextToken) result.nextToken = rest.nextToken
435446
if (rest.listFilters) {
436-
result.filters =
437-
typeof rest.listFilters === 'string'
438-
? JSON.parse(rest.listFilters)
439-
: rest.listFilters
447+
result.filters = parseJsonField(rest.listFilters, 'Filters')
440448
}
441449
if (rest.sortBy) result.sortBy = rest.sortBy
442450
if (rest.sortOrder) result.sortOrder = rest.sortOrder
@@ -449,11 +457,7 @@ Return ONLY the JSON array - no explanations, no markdown code blocks.`,
449457
result.secretValue = rest.secretValue
450458
if (rest.description) result.description = rest.description
451459
result.kmsKeyId = rest.kmsKeyId || undefined
452-
result.tags = rest.createTags
453-
? typeof rest.createTags === 'string'
454-
? JSON.parse(rest.createTags)
455-
: rest.createTags
456-
: undefined
460+
result.tags = rest.createTags ? parseJsonField(rest.createTags, 'Tags') : undefined
457461
break
458462
case 'update_secret':
459463
result.secretId = rest.secretId
@@ -474,12 +478,11 @@ Return ONLY the JSON array - no explanations, no markdown code blocks.`,
474478
break
475479
case 'tag_resource':
476480
result.secretId = rest.secretId
477-
result.tags = typeof rest.tags === 'string' ? JSON.parse(rest.tags) : rest.tags
481+
result.tags = parseJsonField(rest.tags, 'Tags')
478482
break
479483
case 'untag_resource':
480484
result.secretId = rest.secretId
481-
result.tagKeys =
482-
typeof rest.tagKeys === 'string' ? JSON.parse(rest.tagKeys) : rest.tagKeys
485+
result.tagKeys = parseJsonField(rest.tagKeys, 'Tag Keys')
483486
break
484487
case 'restore_secret':
485488
result.secretId = rest.secretId

‎apps/sim/blocks/blocks/textract.ts‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { isRecordLike } from '@sim/utils/object'
12
import { TextractIcon } from '@/components/icons'
23
import {
34
AuthMode,
@@ -246,11 +247,23 @@ interface TextractQueryParam {
246247
Pages?: string[]
247248
}
248249

250+
/** Whether text is a JSON object, which is never a valid query list or question. */
251+
function parsesAsJsonObject(text: string): boolean {
252+
if (!text.startsWith('{')) return false
253+
try {
254+
const parsed: unknown = JSON.parse(text)
255+
return isRecordLike(parsed)
256+
} catch {
257+
return false
258+
}
259+
}
260+
249261
/**
250262
* Accepts a JSON array of query strings or `{Text, Alias, Pages}` objects, or
251263
* one plain-text question per line.
252264
*/
253265
function parseTextractQueries(value: unknown): TextractQueryParam[] {
266+
if (value === undefined || value === null) return []
254267
let raw: unknown = value
255268
if (typeof value === 'string') {
256269
const trimmed = value.trim()
@@ -262,10 +275,15 @@ function parseTextractQueries(value: unknown): TextractQueryParam[] {
262275
throw new Error('Queries must be a JSON array or one question per line')
263276
}
264277
} else {
278+
if (parsesAsJsonObject(trimmed)) {
279+
throw new Error('Queries must be a JSON array or one question per line')
280+
}
265281
raw = trimmed.split('\n')
266282
}
267283
}
268-
if (!Array.isArray(raw)) return []
284+
if (!Array.isArray(raw)) {
285+
throw new Error('Queries must be a JSON array or one question per line')
286+
}
269287
return raw.flatMap((entry): TextractQueryParam[] => {
270288
if (typeof entry === 'string') {
271289
const text = entry.trim()

‎apps/sim/lib/api/contracts/tools/aws/iam-create-role.ts‎

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { isRecordLike } from '@sim/utils/object'
21
import { z } from 'zod'
32
import {
43
iamAssumeRolePolicyDocumentSchema,
@@ -7,6 +6,7 @@ import {
76
iamPolicyArnSchema,
87
iamRoleDescriptionSchema,
98
iamRoleNameSchema,
9+
iamTagsJsonSchema,
1010
} from '@/lib/api/contracts/tools/aws/iam-shared'
1111
import type {
1212
ContractBody,
@@ -29,27 +29,7 @@ const Schema = z.object({
2929
.optional()
3030
.nullable(),
3131
permissionsBoundary: iamPolicyArnSchema.optional().nullable(),
32-
tags: z
33-
.string()
34-
.refine(
35-
(value) => {
36-
try {
37-
const parsed: unknown = JSON.parse(value)
38-
return (
39-
isRecordLike(parsed) &&
40-
Object.keys(parsed).length <= 50 &&
41-
Object.values(parsed).every((tagValue) =>
42-
['string', 'number', 'boolean'].includes(typeof tagValue)
43-
)
44-
)
45-
} catch {
46-
return false
47-
}
48-
},
49-
{ message: 'Tags must be a JSON object of at most 50 key/value pairs with scalar values' }
50-
)
51-
.optional()
52-
.nullable(),
32+
tags: iamTagsJsonSchema.optional().nullable(),
5333
})
5434

5535
const CreateRoleResponseSchema = z.object({

‎apps/sim/lib/api/contracts/tools/aws/iam-create-user.ts‎

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { isRecordLike } from '@sim/utils/object'
21
import { z } from 'zod'
32
import {
43
iamConnectionShape,
54
iamCreatePathSchema,
65
iamPolicyArnSchema,
6+
iamTagsJsonSchema,
77
iamUserName64Schema,
88
} from '@/lib/api/contracts/tools/aws/iam-shared'
99
import type {
@@ -18,27 +18,7 @@ const Schema = z.object({
1818
userName: iamUserName64Schema,
1919
path: iamCreatePathSchema.optional().nullable(),
2020
permissionsBoundary: iamPolicyArnSchema.optional().nullable(),
21-
tags: z
22-
.string()
23-
.refine(
24-
(value) => {
25-
try {
26-
const parsed: unknown = JSON.parse(value)
27-
return (
28-
isRecordLike(parsed) &&
29-
Object.keys(parsed).length <= 50 &&
30-
Object.values(parsed).every((tagValue) =>
31-
['string', 'number', 'boolean'].includes(typeof tagValue)
32-
)
33-
)
34-
} catch {
35-
return false
36-
}
37-
},
38-
{ message: 'Tags must be a JSON object of at most 50 key/value pairs with scalar values' }
39-
)
40-
.optional()
41-
.nullable(),
21+
tags: iamTagsJsonSchema.optional().nullable(),
4222
})
4323

4424
const CreateUserResponseSchema = z.object({

‎apps/sim/lib/api/contracts/tools/aws/iam-shared.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { isRecordLike } from '@sim/utils/object'
12
import { z } from 'zod'
23
import { validateAwsRegion } from '@/lib/core/security/input-validation'
34

@@ -266,3 +267,22 @@ export const iamPaginationResponseShape = {
266267
marker: z.string().nullable(),
267268
count: z.number(),
268269
}
270+
271+
/** IAM tags as a JSON object string: at most 50 pairs with string, number, or boolean values. */
272+
export const iamTagsJsonSchema = z.string().refine(
273+
(value) => {
274+
try {
275+
const parsed: unknown = JSON.parse(value)
276+
return (
277+
isRecordLike(parsed) &&
278+
Object.keys(parsed).length <= 50 &&
279+
Object.values(parsed).every((tagValue) =>
280+
['string', 'number', 'boolean'].includes(typeof tagValue)
281+
)
282+
)
283+
} catch {
284+
return false
285+
}
286+
},
287+
{ message: 'Tags must be a JSON object of at most 50 key/value pairs with scalar values' }
288+
)

0 commit comments

Comments
 (0)