Skip to content

Commit ccaebfb

Browse files
committed
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.
1 parent 9eaeb83 commit ccaebfb

18 files changed

Lines changed: 115 additions & 104 deletions

File tree

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

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,28 @@ 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+
try {
17+
return serializeTags(JSON.parse(trimmed))
18+
} catch {
19+
return trimmed
20+
}
21+
}
1322
if (isRecordLike(tags)) return Object.keys(tags).length > 0 ? JSON.stringify(tags) : undefined
1423
if (!Array.isArray(tags)) return undefined
1524
const obj: Record<string, string> = {}
1625
for (const row of tags) {
1726
const entry = isRecordLike(row) && isRecordLike(row.cells) ? row.cells : row
1827
const key = isRecordLike(entry) ? entry.key : undefined
1928
const value = isRecordLike(entry) ? entry.value : undefined
20-
if (typeof key === 'string' && key && value !== undefined) obj[key] = String(value)
29+
if (typeof key === 'string' && key && value != null) obj[key] = String(value)
2130
}
2231
return Object.keys(obj).length > 0 ? JSON.stringify(obj) : undefined
2332
}

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

Lines changed: 13 additions & 9 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

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,11 +251,12 @@ interface TextractQueryParam {
251251
* one plain-text question per line.
252252
*/
253253
function parseTextractQueries(value: unknown): TextractQueryParam[] {
254+
if (value === undefined || value === null) return []
254255
let raw: unknown = value
255256
if (typeof value === 'string') {
256257
const trimmed = value.trim()
257258
if (!trimmed) return []
258-
if (trimmed.startsWith('[')) {
259+
if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
259260
try {
260261
raw = JSON.parse(trimmed)
261262
} catch {
@@ -265,7 +266,9 @@ function parseTextractQueries(value: unknown): TextractQueryParam[] {
265266
raw = trimmed.split('\n')
266267
}
267268
}
268-
if (!Array.isArray(raw)) return []
269+
if (!Array.isArray(raw)) {
270+
throw new Error('Queries must be a JSON array or one question per line')
271+
}
269272
return raw.flatMap((entry): TextractQueryParam[] => {
270273
if (typeof entry === 'string') {
271274
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+
)

‎apps/sim/lib/api/contracts/tools/aws/ses-send-bulk-email.ts‎

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { z } from 'zod'
2+
import { sesEmailTagsSchema } from '@/lib/api/contracts/tools/aws/ses-shared'
23
import type {
34
ContractBody,
45
ContractBodyInput,
@@ -7,11 +8,6 @@ import type {
78
import { defineRouteContract } from '@/lib/api/contracts/types'
89
import { validateAwsRegion } from '@/lib/core/security/input-validation'
910

10-
/** SES `MessageTag` name or value: 1-256 letters, digits, underscores, or hyphens. */
11-
const sesMessageTagPartSchema = z
12-
.string()
13-
.regex(/^[A-Za-z0-9_-]{1,256}$/, 'Tag names and values may contain only letters, digits, _ and -')
14-
1511
const SendBulkEmailSchema = z.object({
1612
region: z
1713
.string()
@@ -28,9 +24,7 @@ const SendBulkEmailSchema = z.object({
2824
configurationSetName: z.string().nullish(),
2925
replyToAddresses: z.string().nullish(),
3026
fromName: z.string().nullish(),
31-
emailTags: z
32-
.array(z.object({ name: sesMessageTagPartSchema, value: sesMessageTagPartSchema }))
33-
.nullish(),
27+
emailTags: sesEmailTagsSchema,
3428
})
3529

3630
const SendBulkEmailResponseSchema = z.object({

‎apps/sim/lib/api/contracts/tools/aws/ses-send-email.ts‎

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { z } from 'zod'
2+
import { sesEmailTagsSchema } from '@/lib/api/contracts/tools/aws/ses-shared'
23
import type {
34
ContractBody,
45
ContractBodyInput,
@@ -7,11 +8,6 @@ import type {
78
import { defineRouteContract } from '@/lib/api/contracts/types'
89
import { validateAwsRegion } from '@/lib/core/security/input-validation'
910

10-
/** SES `MessageTag` name or value: 1-256 letters, digits, underscores, or hyphens. */
11-
const sesMessageTagPartSchema = z
12-
.string()
13-
.regex(/^[A-Za-z0-9_-]{1,256}$/, 'Tag names and values may contain only letters, digits, _ and -')
14-
1511
const SendEmailSchema = z
1612
.object({
1713
region: z
@@ -32,9 +28,7 @@ const SendEmailSchema = z
3228
replyToAddresses: z.string().nullish(),
3329
configurationSetName: z.string().nullish(),
3430
fromName: z.string().nullish(),
35-
emailTags: z
36-
.array(z.object({ name: sesMessageTagPartSchema, value: sesMessageTagPartSchema }))
37-
.nullish(),
31+
emailTags: sesEmailTagsSchema,
3832
})
3933
.refine((data) => data.bodyText || data.bodyHtml, {
4034
message: 'At least one of bodyText or bodyHtml is required',

‎apps/sim/lib/api/contracts/tools/aws/ses-send-templated-email.ts‎

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { z } from 'zod'
2+
import { sesEmailTagsSchema } from '@/lib/api/contracts/tools/aws/ses-shared'
23
import type {
34
ContractBody,
45
ContractBodyInput,
@@ -7,11 +8,6 @@ import type {
78
import { defineRouteContract } from '@/lib/api/contracts/types'
89
import { validateAwsRegion } from '@/lib/core/security/input-validation'
910

10-
/** SES `MessageTag` name or value: 1-256 letters, digits, underscores, or hyphens. */
11-
const sesMessageTagPartSchema = z
12-
.string()
13-
.regex(/^[A-Za-z0-9_-]{1,256}$/, 'Tag names and values may contain only letters, digits, _ and -')
14-
1511
const SendTemplatedEmailSchema = z.object({
1612
region: z
1713
.string()
@@ -30,9 +26,7 @@ const SendTemplatedEmailSchema = z.object({
3026
configurationSetName: z.string().nullish(),
3127
replyToAddresses: z.string().nullish(),
3228
fromName: z.string().nullish(),
33-
emailTags: z
34-
.array(z.object({ name: sesMessageTagPartSchema, value: sesMessageTagPartSchema }))
35-
.nullish(),
29+
emailTags: sesEmailTagsSchema,
3630
})
3731

3832
const SendTemplatedEmailResponseSchema = z.object({
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { z } from 'zod'
2+
3+
/** SES `MessageTag` name or value: 1-256 letters, digits, underscores, or hyphens. */
4+
const sesMessageTagPartSchema = z
5+
.string()
6+
.regex(/^[A-Za-z0-9_-]{1,256}$/, 'Tag names and values may contain only letters, digits, _ and -')
7+
8+
/** Message tags on an SES send, for filtering its sending events. */
9+
export const sesEmailTagsSchema = z
10+
.array(z.object({ name: sesMessageTagPartSchema, value: sesMessageTagPartSchema }))
11+
.nullish()

0 commit comments

Comments
 (0)