Skip to content

Commit cc9821f

Browse files
committed
fix(lambda): reject empty optional strings across the Lambda contracts
An empty `eventSourceArn` alongside bootstrap servers slipped past the mutual-exclusivity check for the same reason the code-source fields did: the guard tests truthiness, so a defined-but-empty value reads as absent while the operation still forwards it. Rather than patch each field as it surfaces, every optional string field now rejects an empty value. The tool layer already drops `''` before it reaches a contract, so an empty value can only arrive from a malformed direct call, and forwarding it to AWS is never right. `description` is exempt: AWS documents it as "Minimum length of 0", so an empty value legitimately clears it. Both behaviours are covered by tests.
1 parent ea9a81b commit cc9821f

18 files changed

Lines changed: 95 additions & 46 deletions

apps/sim/lib/api/contracts/tools/aws/lambda-add-permission.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,17 @@ const AddPermissionSchema = z.object({
2929
'action must be a Lambda action such as lambda:InvokeFunction'
3030
),
3131
principal: z.string().min(1, 'principal is required'),
32-
sourceArn: z.string().optional(),
33-
sourceAccount: z.string().optional(),
34-
principalOrgId: z.string().optional(),
35-
eventSourceToken: z.string().optional(),
32+
sourceArn: z.string().min(1, 'sourceArn cannot be empty').optional(),
33+
sourceAccount: z.string().min(1, 'sourceAccount cannot be empty').optional(),
34+
principalOrgId: z.string().min(1, 'principalOrgId cannot be empty').optional(),
35+
eventSourceToken: z.string().min(1, 'eventSourceToken cannot be empty').optional(),
3636
functionUrlAuthType: z.enum(['NONE', 'AWS_IAM']).optional(),
3737
qualifier: z
3838
.string()
3939
.min(1, 'qualifier cannot be empty')
4040
.max(128, 'qualifier cannot exceed 128 characters')
4141
.optional(),
42-
revisionId: z.string().optional(),
42+
revisionId: z.string().min(1, 'revisionId cannot be empty').optional(),
4343
})
4444

4545
const AddPermissionResponseSchema = z.object({

apps/sim/lib/api/contracts/tools/aws/lambda-create-event-source-mapping.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const CreateEventSourceMappingSchema = z
1818
.string()
1919
.min(1, 'functionName is required')
2020
.max(256, 'functionName cannot exceed 256 characters'),
21-
eventSourceArn: z.string().optional(),
21+
eventSourceArn: z.string().min(1, 'eventSourceArn cannot be empty').optional(),
2222
enabled: z.boolean().optional(),
2323
batchSize: z.number().int().min(1).max(10000).optional(),
2424
maximumBatchingWindowInSeconds: z.number().int().min(0).max(300).optional(),
@@ -36,9 +36,9 @@ const CreateEventSourceMappingSchema = z
3636
queues: z.array(z.string()).optional(),
3737
functionResponseTypes: z.array(z.enum(['ReportBatchItemFailures'])).optional(),
3838
filterPatterns: z.array(z.string()).optional(),
39-
onSuccessDestination: z.string().optional(),
40-
onFailureDestination: z.string().optional(),
41-
kmsKeyArn: z.string().optional(),
39+
onSuccessDestination: z.string().min(1, 'onSuccessDestination cannot be empty').optional(),
40+
onFailureDestination: z.string().min(1, 'onFailureDestination cannot be empty').optional(),
41+
kmsKeyArn: z.string().min(1, 'kmsKeyArn cannot be empty').optional(),
4242
tags: z.record(z.string(), z.string()).optional(),
4343
sourceAccessConfigurations: z
4444
.array(
@@ -49,11 +49,20 @@ const CreateEventSourceMappingSchema = z
4949
)
5050
.max(22, 'sourceAccessConfigurations accepts at most 22 entries')
5151
.optional(),
52-
documentDbDatabaseName: z.string().optional(),
53-
documentDbCollectionName: z.string().optional(),
52+
documentDbDatabaseName: z.string().min(1, 'documentDbDatabaseName cannot be empty').optional(),
53+
documentDbCollectionName: z
54+
.string()
55+
.min(1, 'documentDbCollectionName cannot be empty')
56+
.optional(),
5457
documentDbFullDocument: z.enum(['UpdateLookup', 'Default']).optional(),
55-
amazonManagedKafkaConsumerGroupId: z.string().optional(),
56-
selfManagedKafkaConsumerGroupId: z.string().optional(),
58+
amazonManagedKafkaConsumerGroupId: z
59+
.string()
60+
.min(1, 'amazonManagedKafkaConsumerGroupId cannot be empty')
61+
.optional(),
62+
selfManagedKafkaConsumerGroupId: z
63+
.string()
64+
.min(1, 'selfManagedKafkaConsumerGroupId cannot be empty')
65+
.optional(),
5766
selfManagedKafkaBootstrapServers: z
5867
.array(z.string().min(1, 'a bootstrap server cannot be empty'))
5968
.optional(),

apps/sim/lib/api/contracts/tools/aws/lambda-create-function.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ const CreateFunctionSchema = z
1818
.min(1, 'functionName is required')
1919
.max(256, 'functionName cannot exceed 256 characters'),
2020
role: z.string().min(1, 'role is required'),
21-
runtime: z.string().optional(),
22-
handler: z.string().optional(),
21+
runtime: z.string().min(1, 'runtime cannot be empty').optional(),
22+
handler: z.string().min(1, 'handler cannot be empty').optional(),
2323
packageType: z.enum(['Zip', 'Image']).optional(),
2424
s3Bucket: z.string().min(1, 's3Bucket cannot be empty').optional(),
2525
s3Key: z.string().min(1, 's3Key cannot be empty').optional(),
@@ -41,11 +41,11 @@ const CreateFunctionSchema = z
4141
vpcSubnetIds: z.array(z.string()).optional(),
4242
vpcSecurityGroupIds: z.array(z.string()).optional(),
4343
tracingMode: z.enum(['Active', 'PassThrough']).optional(),
44-
deadLetterTargetArn: z.string().optional(),
45-
kmsKeyArn: z.string().optional(),
44+
deadLetterTargetArn: z.string().min(1, 'deadLetterTargetArn cannot be empty').optional(),
45+
kmsKeyArn: z.string().min(1, 'kmsKeyArn cannot be empty').optional(),
4646
snapStartApplyOn: z.enum(['PublishedVersions', 'None']).optional(),
4747
logFormat: z.enum(['JSON', 'Text']).optional(),
48-
logGroup: z.string().optional(),
48+
logGroup: z.string().min(1, 'logGroup cannot be empty').optional(),
4949
})
5050
.superRefine((value, ctx) => {
5151
const hasAnyZipField = Boolean(

apps/sim/lib/api/contracts/tools/aws/lambda-invoke.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const InvokeSchema = z.object({
1818
logType: z.enum(['None', 'Tail']).optional(),
1919
clientContext: z
2020
.string()
21+
.min(1, 'clientContext cannot be empty')
2122
.max(3583, 'clientContext cannot exceed 3583 bytes of base64 data')
2223
.optional(),
2324
qualifier: z

apps/sim/lib/api/contracts/tools/aws/lambda-list-aliases.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const ListAliasesSchema = z.object({
1818
.string()
1919
.min(1, 'functionName is required')
2020
.max(256, 'functionName cannot exceed 256 characters'),
21-
aliasFunctionVersion: z.string().optional(),
21+
aliasFunctionVersion: z.string().min(1, 'aliasFunctionVersion cannot be empty').optional(),
2222
})
2323

2424
const ListAliasesResponseSchema = z.object({

apps/sim/lib/api/contracts/tools/aws/lambda-list-event-source-mappings.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const ListEventSourceMappingsSchema = z.object({
1919
.min(1, 'functionName cannot be empty')
2020
.max(256, 'functionName cannot exceed 256 characters')
2121
.optional(),
22-
eventSourceArn: z.string().optional(),
22+
eventSourceArn: z.string().min(1, 'eventSourceArn cannot be empty').optional(),
2323
})
2424

2525
const ListEventSourceMappingsResponseSchema = z.object({

apps/sim/lib/api/contracts/tools/aws/lambda-list-functions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const ListFunctionsSchema = z
1616
...lambdaConnectionFields,
1717
...lambdaPaginationFields,
1818
functionVersion: z.literal('ALL').optional(),
19-
masterRegion: z.string().optional(),
19+
masterRegion: z.string().min(1, 'masterRegion cannot be empty').optional(),
2020
})
2121
.superRefine((value, ctx) => {
2222
if (value.masterRegion && value.functionVersion !== 'ALL') {

apps/sim/lib/api/contracts/tools/aws/lambda-list-layer-versions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const ListLayerVersionsSchema = z.object({
2222
/^(arn:[a-zA-Z0-9-]+:lambda:[a-zA-Z0-9-]+:\d{12}:layer:[a-zA-Z0-9-_]+)$|^[a-zA-Z0-9-_]+$/,
2323
'layerName must be a layer name or a layer ARN'
2424
),
25-
compatibleRuntime: z.string().optional(),
25+
compatibleRuntime: z.string().min(1, 'compatibleRuntime cannot be empty').optional(),
2626
compatibleArchitecture: z.enum(['x86_64', 'arm64']).optional(),
2727
})
2828

apps/sim/lib/api/contracts/tools/aws/lambda-list-layers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { defineRouteContract } from '@/lib/api/contracts/types'
1414
const ListLayersSchema = z.object({
1515
...lambdaConnectionFields,
1616
...lambdaSmallPaginationFields,
17-
compatibleRuntime: z.string().optional(),
17+
compatibleRuntime: z.string().min(1, 'compatibleRuntime cannot be empty').optional(),
1818
compatibleArchitecture: z.enum(['x86_64', 'arm64']).optional(),
1919
})
2020

apps/sim/lib/api/contracts/tools/aws/lambda-publish-version.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ const PublishVersionSchema = z.object({
1616
.string()
1717
.min(1, 'functionName is required')
1818
.max(256, 'functionName cannot exceed 256 characters'),
19-
codeSha256: z.string().optional(),
19+
codeSha256: z.string().min(1, 'codeSha256 cannot be empty').optional(),
2020
description: z.string().max(256, 'description cannot exceed 256 characters').optional(),
21-
revisionId: z.string().optional(),
21+
revisionId: z.string().min(1, 'revisionId cannot be empty').optional(),
2222
})
2323

2424
const PublishVersionResponseSchema = z.object({

0 commit comments

Comments
 (0)