Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions apps/docs/content/docs/integrations/jira.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,7 @@ Search for Jira issues using JQL (Jira Query Language)
| `jql` | string | Yes | JQL query string to search for issues \(e.g., "project = PROJ AND status = Open"\) |
| `nextPageToken` | string | No | Cursor token for the next page of results. Omit for the first page. |
| `maxResults` | number | No | Maximum number of results to return per page \(default: 50\) |
| `fields` | array | No | Array of field names to return \(default: all fields\). |
| `fields` | array | No | Array of field names to return \(default: all fields\). When set, each issue also includes descriptionAdf, customFields, attachments, issuelinks, and subtasks. |
| `expand` | string | No | Comma-separated expansions: renderedFields, names, schema, transitions, operations, editmeta, changelog |

#### Output
Expand All @@ -562,8 +562,8 @@ Search for Jira issues using JQL (Jira Query Language)
| ↳ `self` | string | REST API URL for this issue |
| ↳ `summary` | string | Issue summary |
| ↳ `description` | string | Issue description text \(extracted from ADF\) |
| ↳ `descriptionAdf` | json | Issue description as the raw Atlassian Document Format \(ADF\) document |
| ↳ `renderedDescription` | string | Issue description rendered as HTML \(requires the renderedFields expansion\) |
| ↳ `descriptionAdf` | json | Issue description as the raw Atlassian Document Format \(ADF\) document. Only included when the fields param is set |
| ↳ `renderedDescription` | string | Issue description rendered as HTML. Only included when expand includes renderedFields |
| ↳ `environment` | string | Environment text \(extracted from ADF\) |
| ↳ `status` | object | Issue status |
| ↳ `id` | string | Status ID |
Expand Down Expand Up @@ -644,7 +644,7 @@ Search for Jira issues using JQL (Jira Query Language)
| ↳ `id` | string | Parent issue ID |
| ↳ `key` | string | Parent issue key |
| ↳ `summary` | string | Parent issue summary |
| ↳ `issuelinks` | array | Linked issues |
| ↳ `issuelinks` | array | Linked issues. Only included when the fields param is set |
| ↳ `id` | string | Issue link ID |
| ↳ `type` | object | Link type information |
| ↳ `id` | string | Link type ID |
Expand All @@ -661,7 +661,7 @@ Search for Jira issues using JQL (Jira Query Language)
| ↳ `key` | string | Issue key |
| ↳ `statusName` | string | Issue status name |
| ↳ `summary` | string | Issue summary |
| ↳ `subtasks` | array | Subtask issues |
| ↳ `subtasks` | array | Subtask issues. Only included when the fields param is set |
| ↳ `id` | string | Subtask issue ID |
| ↳ `key` | string | Subtask issue key |
| ↳ `summary` | string | Subtask summary |
Expand All @@ -673,7 +673,7 @@ Search for Jira issues using JQL (Jira Query Language)
| ↳ `watches` | object | Watch information |
| ↳ `watchCount` | number | Number of watchers |
| ↳ `isWatching` | boolean | Whether the current user is watching |
| ↳ `attachments` | array | Attachment metadata \(no file contents\) |
| ↳ `attachments` | array | Attachment metadata \(no file contents\). Only included when the fields param is set |
| ↳ `id` | string | Attachment ID |
| ↳ `filename` | string | Attachment file name |
| ↳ `mimeType` | string | MIME type of the attachment |
Expand All @@ -690,7 +690,7 @@ Search for Jira issues using JQL (Jira Query Language)
| ↳ `timeZone` | string | User timezone |
| ↳ `authorName` | string | Attachment author display name |
| ↳ `created` | string | ISO 8601 timestamp when the attachment was created |
| ↳ `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. |
| ↳ `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. |
| `nextPageToken` | string | Cursor token for the next page. Null when no more results. |
| `isLast` | boolean | Whether this is the last page of results |
| `names` | json | Map of field ID to display name \(e.g., customfield_10020 → "Sprint"\). Only returned when expand includes names. |
Expand Down
4 changes: 2 additions & 2 deletions apps/docs/content/docs/integrations/trello.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -363,8 +363,8 @@ Update an existing card on Trello
| `start` | string | No | Start date \(ISO 8601 format\) |
| `pos` | string | No | Position of the card in its list \(top, bottom, or positive float\) |
| `idBoard` | string | No | Trello board ID to move the card to \(use with idList\) |
| `labelIds` | array | No | Replace the card labels with these label IDs. An empty list leaves labels unchanged; use Remove Label to take one off |
| `memberIds` | array | No | Replace the card members with these member IDs. An empty list leaves members unchanged; use Remove Member to take one off |
| `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 |
| `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 |

#### Output

Expand Down
11 changes: 11 additions & 0 deletions apps/sim/blocks/blocks/aws-stale-inputs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,15 @@ describe('AWS blocks keep stale subBlock values out of tool input', () => {
expect(body.policyUsageFilter).toBeUndefined()
expect(awsIamListPoliciesContract.body?.safeParse(body).success).toBe(true)
})

it('create user rejects a JSON-array tags string with no valid entries', () => {
expect(() =>
executorParams(IAMBlock, {
...AWS,
operation: 'create_user',
userName: 'alice',
tags: '[{"name":"team","val":"core"}]',
})
).toThrow('Tags must be a list of {"key", "value"} entries or a JSON object')
})
})
23 changes: 19 additions & 4 deletions apps/sim/blocks/blocks/iam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,34 @@ import { AuthMode, IntegrationType } from '@/blocks/types'
import { withSavedCredential } from '@/blocks/utils'

/**
* Converts the Tags table, a `[{ key, value }]` list, a plain object, or a JSON object
* string into the JSON object string the tools accept.
* Converts the Tags table, a `[{ key, value }]` list (or its JSON string), a plain
* object, or a JSON object string into the JSON object string the tools accept.
*/
function serializeTags(tags: unknown): string | undefined {
if (typeof tags === 'string') return tags.trim() ? tags : undefined
if (typeof tags === 'string') {
const trimmed = tags.trim()
if (!trimmed) return undefined
if (!trimmed.startsWith('[')) return trimmed
let parsed: unknown
try {
parsed = JSON.parse(trimmed)
} catch {
return trimmed
}
const serialized = serializeTags(parsed)
if (serialized === undefined && Array.isArray(parsed) && parsed.length > 0) {
throw new Error('Tags must be a list of {"key", "value"} entries or a JSON object')
}
return serialized
}
if (isRecordLike(tags)) return Object.keys(tags).length > 0 ? JSON.stringify(tags) : undefined
if (!Array.isArray(tags)) return undefined
const obj: Record<string, string> = {}
for (const row of tags) {
const entry = isRecordLike(row) && isRecordLike(row.cells) ? row.cells : row
const key = isRecordLike(entry) ? entry.key : undefined
const value = isRecordLike(entry) ? entry.value : undefined
if (typeof key === 'string' && key && value !== undefined) obj[key] = String(value)
if (typeof key === 'string' && key && value !== undefined) obj[key] = String(value ?? '')
}
return Object.keys(obj).length > 0 ? JSON.stringify(obj) : undefined
}
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/blocks/blocks/jira.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1619,7 +1619,7 @@ Return ONLY the comment text - no explanations.`,
issues: {
type: 'json',
description:
'Array of matching issues with key, summary, description, descriptionAdf, status, assignee, reporter, priority, labels, components, fixVersions, parent, subtasks, issuelinks, attachments, timetracking, customFields, dates',
'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',
},

comments: {
Expand Down
27 changes: 15 additions & 12 deletions apps/sim/blocks/blocks/secrets_manager.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import { getErrorMessage } from '@sim/utils/errors'
import { SecretsManagerIcon } from '@/components/icons'
import type { BlockConfig, BlockMeta } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
import { withSavedCredential } from '@/blocks/utils'

/** Parses a JSON field typed into the block, naming the field when it is invalid. */
function parseJsonField(value: unknown, fieldName: string): unknown {
if (typeof value !== 'string') return value
try {
return JSON.parse(value)
} catch (error) {
throw new Error(`Invalid JSON in ${fieldName}: ${getErrorMessage(error)}`)
}
}

export const SecretsManagerBlock: BlockConfig = {
type: 'secrets_manager',
hideFromToolbar: true,
Expand Down Expand Up @@ -433,10 +444,7 @@ Return ONLY the JSON array - no explanations, no markdown code blocks.`,
}
if (rest.nextToken) result.nextToken = rest.nextToken
if (rest.listFilters) {
result.filters =
typeof rest.listFilters === 'string'
? JSON.parse(rest.listFilters)
: rest.listFilters
result.filters = parseJsonField(rest.listFilters, 'Filters')
}
if (rest.sortBy) result.sortBy = rest.sortBy
if (rest.sortOrder) result.sortOrder = rest.sortOrder
Expand All @@ -449,11 +457,7 @@ Return ONLY the JSON array - no explanations, no markdown code blocks.`,
result.secretValue = rest.secretValue
if (rest.description) result.description = rest.description
result.kmsKeyId = rest.kmsKeyId || undefined
result.tags = rest.createTags
? typeof rest.createTags === 'string'
? JSON.parse(rest.createTags)
: rest.createTags
: undefined
result.tags = rest.createTags ? parseJsonField(rest.createTags, 'Tags') : undefined
Comment thread
waleedlatif1 marked this conversation as resolved.
Resolved
break
case 'update_secret':
result.secretId = rest.secretId
Expand All @@ -474,12 +478,11 @@ Return ONLY the JSON array - no explanations, no markdown code blocks.`,
break
case 'tag_resource':
result.secretId = rest.secretId
result.tags = typeof rest.tags === 'string' ? JSON.parse(rest.tags) : rest.tags
result.tags = parseJsonField(rest.tags, 'Tags')
break
case 'untag_resource':
result.secretId = rest.secretId
result.tagKeys =
typeof rest.tagKeys === 'string' ? JSON.parse(rest.tagKeys) : rest.tagKeys
result.tagKeys = parseJsonField(rest.tagKeys, 'Tag Keys')
break
case 'restore_secret':
result.secretId = rest.secretId
Expand Down
20 changes: 19 additions & 1 deletion apps/sim/blocks/blocks/textract.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isRecordLike } from '@sim/utils/object'
import { TextractIcon } from '@/components/icons'
import {
AuthMode,
Expand Down Expand Up @@ -246,11 +247,23 @@ interface TextractQueryParam {
Pages?: string[]
}

/** Whether text is a JSON object, which is never a valid query list or question. */
function parsesAsJsonObject(text: string): boolean {
if (!text.startsWith('{')) return false
try {
const parsed: unknown = JSON.parse(text)
return isRecordLike(parsed)
} catch {
return false
}
}

/**
* Accepts a JSON array of query strings or `{Text, Alias, Pages}` objects, or
* one plain-text question per line.
*/
function parseTextractQueries(value: unknown): TextractQueryParam[] {
if (value === undefined || value === null) return []
let raw: unknown = value
if (typeof value === 'string') {
const trimmed = value.trim()
Expand All @@ -262,10 +275,15 @@ function parseTextractQueries(value: unknown): TextractQueryParam[] {
throw new Error('Queries must be a JSON array or one question per line')
}
} else {
if (parsesAsJsonObject(trimmed)) {
throw new Error('Queries must be a JSON array or one question per line')
}
raw = trimmed.split('\n')
}
}
if (!Array.isArray(raw)) return []
if (!Array.isArray(raw)) {
throw new Error('Queries must be a JSON array or one question per line')
Comment thread
waleedlatif1 marked this conversation as resolved.
Resolved
}
return raw.flatMap((entry): TextractQueryParam[] => {
if (typeof entry === 'string') {
const text = entry.trim()
Expand Down
24 changes: 2 additions & 22 deletions apps/sim/lib/api/contracts/tools/aws/iam-create-role.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { isRecordLike } from '@sim/utils/object'
import { z } from 'zod'
import {
iamAssumeRolePolicyDocumentSchema,
Expand All @@ -7,6 +6,7 @@ import {
iamPolicyArnSchema,
iamRoleDescriptionSchema,
iamRoleNameSchema,
iamTagsJsonSchema,
} from '@/lib/api/contracts/tools/aws/iam-shared'
import type {
ContractBody,
Expand All @@ -29,27 +29,7 @@ const Schema = z.object({
.optional()
.nullable(),
permissionsBoundary: iamPolicyArnSchema.optional().nullable(),
tags: z
.string()
.refine(
(value) => {
try {
const parsed: unknown = JSON.parse(value)
return (
isRecordLike(parsed) &&
Object.keys(parsed).length <= 50 &&
Object.values(parsed).every((tagValue) =>
['string', 'number', 'boolean'].includes(typeof tagValue)
)
)
} catch {
return false
}
},
{ message: 'Tags must be a JSON object of at most 50 key/value pairs with scalar values' }
)
.optional()
.nullable(),
tags: iamTagsJsonSchema.optional().nullable(),
})

const CreateRoleResponseSchema = z.object({
Expand Down
24 changes: 2 additions & 22 deletions apps/sim/lib/api/contracts/tools/aws/iam-create-user.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { isRecordLike } from '@sim/utils/object'
import { z } from 'zod'
import {
iamConnectionShape,
iamCreatePathSchema,
iamPolicyArnSchema,
iamTagsJsonSchema,
iamUserName64Schema,
} from '@/lib/api/contracts/tools/aws/iam-shared'
import type {
Expand All @@ -18,27 +18,7 @@ const Schema = z.object({
userName: iamUserName64Schema,
path: iamCreatePathSchema.optional().nullable(),
permissionsBoundary: iamPolicyArnSchema.optional().nullable(),
tags: z
.string()
.refine(
(value) => {
try {
const parsed: unknown = JSON.parse(value)
return (
isRecordLike(parsed) &&
Object.keys(parsed).length <= 50 &&
Object.values(parsed).every((tagValue) =>
['string', 'number', 'boolean'].includes(typeof tagValue)
)
)
} catch {
return false
}
},
{ message: 'Tags must be a JSON object of at most 50 key/value pairs with scalar values' }
)
.optional()
.nullable(),
tags: iamTagsJsonSchema.optional().nullable(),
})

const CreateUserResponseSchema = z.object({
Expand Down
20 changes: 20 additions & 0 deletions apps/sim/lib/api/contracts/tools/aws/iam-shared.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isRecordLike } from '@sim/utils/object'
import { z } from 'zod'
import { validateAwsRegion } from '@/lib/core/security/input-validation'

Expand Down Expand Up @@ -266,3 +267,22 @@ export const iamPaginationResponseShape = {
marker: z.string().nullable(),
count: z.number(),
}

/** IAM tags as a JSON object string: at most 50 pairs with string, number, or boolean values. */
export const iamTagsJsonSchema = z.string().refine(
(value) => {
try {
const parsed: unknown = JSON.parse(value)
return (
isRecordLike(parsed) &&
Object.keys(parsed).length <= 50 &&
Object.values(parsed).every((tagValue) =>
['string', 'number', 'boolean'].includes(typeof tagValue)
)
)
} catch {
return false
}
},
{ message: 'Tags must be a JSON object of at most 50 key/value pairs with scalar values' }
)
10 changes: 2 additions & 8 deletions apps/sim/lib/api/contracts/tools/aws/ses-send-bulk-email.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { z } from 'zod'
import { sesEmailTagsSchema } from '@/lib/api/contracts/tools/aws/ses-shared'
import type {
ContractBody,
ContractBodyInput,
Expand All @@ -7,11 +8,6 @@ import type {
import { defineRouteContract } from '@/lib/api/contracts/types'
import { validateAwsRegion } from '@/lib/core/security/input-validation'

/** SES `MessageTag` name or value: 1-256 letters, digits, underscores, or hyphens. */
const sesMessageTagPartSchema = z
.string()
.regex(/^[A-Za-z0-9_-]{1,256}$/, 'Tag names and values may contain only letters, digits, _ and -')

const SendBulkEmailSchema = z.object({
region: z
.string()
Expand All @@ -28,9 +24,7 @@ const SendBulkEmailSchema = z.object({
configurationSetName: z.string().nullish(),
replyToAddresses: z.string().nullish(),
fromName: z.string().nullish(),
emailTags: z
.array(z.object({ name: sesMessageTagPartSchema, value: sesMessageTagPartSchema }))
.nullish(),
emailTags: sesEmailTagsSchema,
})

const SendBulkEmailResponseSchema = z.object({
Expand Down
Loading
Loading