diff --git a/docs/features/parser.md b/docs/features/parser.md
index 89efc57165..508a0ea75f 100644
--- a/docs/features/parser.md
+++ b/docs/features/parser.md
@@ -53,55 +53,58 @@ When using the decorator or middleware, you can specify a schema to parse the ev
!!! note "Looking for other libraries?"
The built-in schemas are defined using Zod, if you would like us to support other libraries like [valibot](https://valibot.dev){target="_blank"} please [open an issue](https://github.com/aws-powertools/powertools-lambda-typescript/issues/new?template=feature_request.yml){target="_blank"} and we will consider it based on the community's feedback.
-| Model name | Description |
-| -------------------------------------------- | ------------------------------------------------------------------------------------- |
-| **AlbSchema** | Lambda Event Source payload for Amazon Application Load Balancer |
-| **APIGatewayProxyEventSchema** | Lambda Event Source payload for Amazon API Gateway |
-| **APIGatewayRequestAuthorizerEventSchema** | Lambda Event Source payload for Amazon API Gateway Request Authorizer |
-| **APIGatewayTokenAuthorizerEventSchema** | Lambda Event Source payload for Amazon API Gateway Token Authorizer |
-| **APIGatewayProxyEventV2Schema** | Lambda Event Source payload for Amazon API Gateway v2 payload |
-| **APIGatewayProxyWebsocketEventSchema** | Lambda Event Source payload for Amazon API Gateway WebSocket events |
-| **APIGatewayRequestAuthorizerEventV2Schema** | Lambda Event Source payload for Amazon API Gateway v2 Authorizer |
-| **AppSyncResolverSchema** | Lambda Event Source payload for AWS AppSync GraphQL API resolver |
-| **AppSyncBatchResolverSchema** | Lambda Event Source payload for AWS AppSync GraphQL API batch resolver |
-| **AppSyncEventsPublishSchema** | Lambda Event Source payload for AWS AppSync Events API `PUBLISH` operation |
-| **AppSyncEventsSubscribeSchema** | Lambda Event Source payload for AWS AppSync Events API `SUBSCRIBE` operation |
-| **CloudFormationCustomResourceCreateSchema** | Lambda Event Source payload for AWS CloudFormation `CREATE` operation |
-| **CloudFormationCustomResourceUpdateSchema** | Lambda Event Source payload for AWS CloudFormation `UPDATE` operation |
-| **CloudFormationCustomResourceDeleteSchema** | Lambda Event Source payload for AWS CloudFormation `DELETE` operation |
-| **CloudwatchLogsSchema** | Lambda Event Source payload for Amazon CloudWatch Logs |
-| **PreSignupTriggerSchema** | Lambda Event Source payload for Amazon Cognito Pre Sign-up trigger |
-| **PostConfirmationTriggerSchema** | Lambda Event Source payload for Amazon Cognito Post Confirmation trigger |
-| **PreTokenGenerationTriggerSchema** | Lambda Event Source payload for Amazon Cognito Pre Token Generation trigger |
-| **CustomMessageTriggerSchema** | Lambda Event Source payload for Amazon Cognito Custom Message trigger |
-| **MigrateUserTriggerSchema** | Lambda Event Source payload for Amazon Cognito User Migration trigger |
-| **CustomSMSTriggerSchema** | Lambda Event Source payload for Amazon Cognito Custom SMS trigger |
-| **CustomEmailTriggerSchema** | Lambda Event Source payload for Amazon Cognito Custom Email trigger |
-| **DefineAuthChallengeTriggerSchema** | Lambda Event Source payload for Amazon Cognito Define Auth Challenge trigger |
-| **CreateAuthChallengeTriggerSchema** | Lambda Event Source payload for Amazon Cognito Create Auth Challenge trigger |
-| **VerifyAuthChallengeResponseTriggerSchema** | Lambda Event Source payload for Amazon Cognito Verify Auth Challenge Response trigger |
-| **PreTokenGenerationTriggerSchemaV1** | Lambda Event Source payload for Amazon Cognito Pre Token Generation trigger v1 |
-| **PreTokenGenerationTriggerSchemaV2AndV3** | Lambda Event Source payload for Amazon Cognito Pre Token Generation trigger v2 and v3 |
-| **ConnectOutboundCampaignsSchema** | Lambda Event Source payload for Amazon Connect Outbound Campaigns custom actions |
-| **DynamoDBStreamSchema** | Lambda Event Source payload for Amazon DynamoDB Streams |
-| **EventBridgeSchema** | Lambda Event Source payload for Amazon EventBridge |
-| **KafkaMskEventSchema** | Lambda Event Source payload for AWS MSK payload |
-| **KafkaSelfManagedEventSchema** | Lambda Event Source payload for self managed Kafka payload |
-| **KinesisDataStreamSchema** | Lambda Event Source payload for Amazon Kinesis Data Streams |
-| **KinesisFirehoseSchema** | Lambda Event Source payload for Amazon Kinesis Firehose |
-| **KinesisDynamoDBStreamSchema** | Lambda Event Source payload for DynamodbStream record wrapped in Kinesis Data stream |
-| **KinesisFirehoseSqsSchema** | Lambda Event Source payload for SQS messages wrapped in Kinesis Firehose records |
-| **LambdaFunctionUrlSchema** | Lambda Event Source payload for Lambda Function URL payload |
-| **S3EventNotificationEventBridgeSchema** | Lambda Event Source payload for Amazon S3 Event Notification to EventBridge. |
-| **S3Schema** | Lambda Event Source payload for Amazon S3 |
-| **S3ObjectLambdaEvent** | Lambda Event Source payload for Amazon S3 Object Lambda |
-| **S3SqsEventNotificationSchema** | Lambda Event Source payload for S3 event notifications wrapped in SQS event (S3->SQS) |
-| **SesSchema** | Lambda Event Source payload for Amazon Simple Email Service |
-| **SnsSchema** | Lambda Event Source payload for Amazon Simple Notification Service |
-| **SqsSchema** | Lambda Event Source payload for Amazon SQS |
-| **TransferFamilySchema** | Lambda Event Source payload for AWS Transfer Family events |
-| **VpcLatticeSchema** | Lambda Event Source payload for Amazon VPC Lattice |
-| **VpcLatticeV2Schema** | Lambda Event Source payload for Amazon VPC Lattice v2 payload |
+| Model name | Description |
+| ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
+| **AlbSchema** | Lambda Event Source payload for Amazon Application Load Balancer |
+| **APIGatewayProxyEventSchema** | Lambda Event Source payload for Amazon API Gateway |
+| **APIGatewayRequestAuthorizerEventSchema** | Lambda Event Source payload for Amazon API Gateway Request Authorizer |
+| **APIGatewayTokenAuthorizerEventSchema** | Lambda Event Source payload for Amazon API Gateway Token Authorizer |
+| **APIGatewayProxyEventV2Schema** | Lambda Event Source payload for Amazon API Gateway v2 payload |
+| **APIGatewayProxyWebsocketEventSchema** | Lambda Event Source payload for Amazon API Gateway WebSocket events |
+| **APIGatewayRequestAuthorizerEventV2Schema** | Lambda Event Source payload for Amazon API Gateway v2 Authorizer |
+| **AppSyncResolverSchema** | Lambda Event Source payload for AWS AppSync GraphQL API resolver |
+| **AppSyncBatchResolverSchema** | Lambda Event Source payload for AWS AppSync GraphQL API batch resolver |
+| **AppSyncEventsPublishSchema** | Lambda Event Source payload for AWS AppSync Events API `PUBLISH` operation |
+| **AppSyncEventsSubscribeSchema** | Lambda Event Source payload for AWS AppSync Events API `SUBSCRIBE` operation |
+| **CloudFormationCustomResourceCreateSchema** | Lambda Event Source payload for AWS CloudFormation `CREATE` operation |
+| **CloudFormationCustomResourceUpdateSchema** | Lambda Event Source payload for AWS CloudFormation `UPDATE` operation |
+| **CloudFormationCustomResourceDeleteSchema** | Lambda Event Source payload for AWS CloudFormation `DELETE` operation |
+| **CloudwatchLogsSchema** | Lambda Event Source payload for Amazon CloudWatch Logs |
+| **PreSignupTriggerSchema** | Lambda Event Source payload for Amazon Cognito Pre Sign-up trigger |
+| **PostConfirmationTriggerSchema** | Lambda Event Source payload for Amazon Cognito Post Confirmation trigger |
+| **PreTokenGenerationTriggerSchema** | Lambda Event Source payload for Amazon Cognito Pre Token Generation trigger |
+| **CustomMessageTriggerSchema** | Lambda Event Source payload for Amazon Cognito Custom Message trigger |
+| **MigrateUserTriggerSchema** | Lambda Event Source payload for Amazon Cognito User Migration trigger |
+| **CustomSMSTriggerSchema** | Lambda Event Source payload for Amazon Cognito Custom SMS trigger |
+| **CustomEmailTriggerSchema** | Lambda Event Source payload for Amazon Cognito Custom Email trigger |
+| **DefineAuthChallengeTriggerSchema** | Lambda Event Source payload for Amazon Cognito Define Auth Challenge trigger |
+| **CreateAuthChallengeTriggerSchema** | Lambda Event Source payload for Amazon Cognito Create Auth Challenge trigger |
+| **VerifyAuthChallengeResponseTriggerSchema** | Lambda Event Source payload for Amazon Cognito Verify Auth Challenge Response trigger |
+| **PreTokenGenerationTriggerSchemaV1** | Lambda Event Source payload for Amazon Cognito Pre Token Generation trigger v1 |
+| **PreTokenGenerationTriggerSchemaV2AndV3** | Lambda Event Source payload for Amazon Cognito Pre Token Generation trigger v2 and v3 |
+| **ConnectOutboundCampaignsSchema** | Lambda Event Source payload for Amazon Connect Outbound Campaigns custom actions |
+| **DynamoDBStreamSchema** | Lambda Event Source payload for Amazon DynamoDB Streams |
+| **EventBridgeSchema** | Lambda Event Source payload for Amazon EventBridge |
+| **EventBridgeWithMetadataSchema** | Nonempty batch of EventBridge `WITH_METADATA` records containing standard EventBridge events. |
+| **EventBridgeWithMetadataRecordSchema** | One EventBridge `WITH_METADATA` record, including single-object delivery with `MaxBatchSize: 1`. |
+| **EventBridgeWithMetadataSystemMetadataSchema** | EventBridge delivery metadata; requires `aws:DeliveryType` (`LIVE` or `REPLAY`) and keeps optional fields as strings. |
+| **KafkaMskEventSchema** | Lambda Event Source payload for AWS MSK payload |
+| **KafkaSelfManagedEventSchema** | Lambda Event Source payload for self managed Kafka payload |
+| **KinesisDataStreamSchema** | Lambda Event Source payload for Amazon Kinesis Data Streams |
+| **KinesisFirehoseSchema** | Lambda Event Source payload for Amazon Kinesis Firehose |
+| **KinesisDynamoDBStreamSchema** | Lambda Event Source payload for DynamodbStream record wrapped in Kinesis Data stream |
+| **KinesisFirehoseSqsSchema** | Lambda Event Source payload for SQS messages wrapped in Kinesis Firehose records |
+| **LambdaFunctionUrlSchema** | Lambda Event Source payload for Lambda Function URL payload |
+| **S3EventNotificationEventBridgeSchema** | Lambda Event Source payload for Amazon S3 Event Notification to EventBridge. |
+| **S3Schema** | Lambda Event Source payload for Amazon S3 |
+| **S3ObjectLambdaEvent** | Lambda Event Source payload for Amazon S3 Object Lambda |
+| **S3SqsEventNotificationSchema** | Lambda Event Source payload for S3 event notifications wrapped in SQS event (S3->SQS) |
+| **SesSchema** | Lambda Event Source payload for Amazon Simple Email Service |
+| **SnsSchema** | Lambda Event Source payload for Amazon Simple Notification Service |
+| **SqsSchema** | Lambda Event Source payload for Amazon SQS |
+| **TransferFamilySchema** | Lambda Event Source payload for AWS Transfer Family events |
+| **VpcLatticeSchema** | Lambda Event Source payload for Amazon VPC Lattice |
+| **VpcLatticeV2Schema** | Lambda Event Source payload for Amazon VPC Lattice v2 payload |
### Extend built-in schemas
@@ -221,22 +224,23 @@ Parser comes with the following built-in Zod envelopes:
!!! note "Looking for other libraries?"
The built-in schemas are defined using Zod, if you would like us to support other libraries like [valibot](https://valibot.dev){target="_blank"} please [open an issue](https://github.com/aws-powertools/powertools-lambda-typescript/issues/new?template=feature_request.yml){target="_blank"} and we will consider it based on the community's feedback.
-| Envelope name | Behaviour |
-| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| **ApiGatewayEnvelope** | 1. Parses data using `APIGatewayProxyEventSchema`.
2. Parses `body` key using your schema and returns it. |
-| **ApiGatewayV2Envelope** | 1. Parses data using `APIGatewayProxyEventV2Schema`.
2. Parses `body` key using your schema and returns it. |
-| **CloudWatchEnvelope** | 1. Parses data using `CloudwatchLogsSchema` which will base64 decode and decompress it.
2. Parses records in `message` key using your schema and return them in a list. |
-| **DynamoDBStreamEnvelope** | 1. Parses data using `DynamoDBStreamSchema`.
2. Parses records in `NewImage` and `OldImage` keys using your schema.
3. Returns a list with a dictionary containing `NewImage` and `OldImage` keys |
-| **EventBridgeEnvelope** | 1. Parses data using `EventBridgeSchema`.
2. Parses `detail` key using your schema and returns it. |
-| **KafkaEnvelope** | 1. Parses data using `KafkaRecordSchema`.
2. Parses `value` key using your schema and returns it. |
-| **KinesisEnvelope** | 1. Parses data using `KinesisDataStreamSchema` which will base64 decode it.
2. Parses records in `Records` key using your schema and returns them in a list. |
-| **KinesisFirehoseEnvelope** | 1. Parses data using `KinesisFirehoseSchema` which will base64 decode it.
2. Parses records in `Records` key using your schema and returns them in a list. |
-| **LambdaFunctionUrlEnvelope** | 1. Parses data using `LambdaFunctionUrlSchema`.
2. Parses `body` key using your schema and returns it. |
-| **SnsEnvelope** | 1. Parses data using `SnsSchema`.
2. Parses records in `body` key using your schema and return them in a list. |
-| **SnsSqsEnvelope** | 1. Parses data using `SqsSchema`.
2. Parses SNS records in `body` key using `SnsNotificationSchema`.
3. Parses data in `Message` key using your schema and return them in a list. |
-| **SqsEnvelope** | 1. Parses data using `SqsSchema`.
2. Parses records in `body` key using your schema and return them in a list. |
-| **VpcLatticeEnvelope** | 1. Parses data using `VpcLatticeSchema`.
2. Parses `value` key using your schema and returns it. |
-| **VpcLatticeV2Envelope** | 1. Parses data using `VpcLatticeSchema`.
2. Parses `value` key using your schema and returns it. |
+| Envelope name | Behaviour |
+| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **ApiGatewayEnvelope** | 1. Parses data using `APIGatewayProxyEventSchema`.
2. Parses `body` key using your schema and returns it. |
+| **ApiGatewayV2Envelope** | 1. Parses data using `APIGatewayProxyEventV2Schema`.
2. Parses `body` key using your schema and returns it. |
+| **CloudWatchEnvelope** | 1. Parses data using `CloudwatchLogsSchema` which will base64 decode and decompress it.
2. Parses records in `message` key using your schema and return them in a list. |
+| **DynamoDBStreamEnvelope** | 1. Parses data using `DynamoDBStreamSchema`.
2. Parses records in `NewImage` and `OldImage` keys using your schema.
3. Returns a list with a dictionary containing `NewImage` and `OldImage` keys |
+| **EventBridgeEnvelope** | 1. Parses data using `EventBridgeSchema`.
2. Parses `detail` key using your schema and returns it. |
+| **EventBridgeWithMetadataEnvelope** | 1. Validates a nonempty `WITH_METADATA` batch.
2. Parses each `Data.detail` using your schema and returns the results in input order. |
+| **KafkaEnvelope** | 1. Parses data using `KafkaRecordSchema`.
2. Parses `value` key using your schema and returns it. |
+| **KinesisEnvelope** | 1. Parses data using `KinesisDataStreamSchema` which will base64 decode it.
2. Parses records in `Records` key using your schema and returns them in a list. |
+| **KinesisFirehoseEnvelope** | 1. Parses data using `KinesisFirehoseSchema` which will base64 decode it.
2. Parses records in `Records` key using your schema and returns them in a list. |
+| **LambdaFunctionUrlEnvelope** | 1. Parses data using `LambdaFunctionUrlSchema`.
2. Parses `body` key using your schema and returns it. |
+| **SnsEnvelope** | 1. Parses data using `SnsSchema`.
2. Parses records in `body` key using your schema and return them in a list. |
+| **SnsSqsEnvelope** | 1. Parses data using `SqsSchema`.
2. Parses SNS records in `body` key using `SnsNotificationSchema`.
3. Parses data in `Message` key using your schema and return them in a list. |
+| **SqsEnvelope** | 1. Parses data using `SqsSchema`.
2. Parses records in `body` key using your schema and return them in a list. |
+| **VpcLatticeEnvelope** | 1. Parses data using `VpcLatticeSchema`.
2. Parses `value` key using your schema and returns it. |
+| **VpcLatticeV2Envelope** | 1. Parses data using `VpcLatticeSchema`.
2. Parses `value` key using your schema and returns it. |
## Safe parsing
diff --git a/packages/parser/src/envelopes/eventbridge-with-metadata.ts b/packages/parser/src/envelopes/eventbridge-with-metadata.ts
new file mode 100644
index 0000000000..7699ca37ba
--- /dev/null
+++ b/packages/parser/src/envelopes/eventbridge-with-metadata.ts
@@ -0,0 +1,70 @@
+import type { ZodType } from 'zod';
+import { ParseError } from '../errors.js';
+import { EventBridgeSchema } from '../schemas/eventbridge.js';
+import { EventBridgeWithMetadataRecordSchema } from '../schemas/eventbridge-with-metadata.js';
+import type { ParsedResult } from '../types/index.js';
+import { envelopeDiscriminator } from './envelope.js';
+
+/**
+ * Validates a WITH_METADATA batch and extracts each record's parsed `Data.detail`.
+ *
+ * Returns details in input order. Invalid records fail the whole batch, with
+ * validation paths relative to the original input. Use the record schema
+ * directly for single-object delivery or to retain delivery metadata.
+ */
+const EventBridgeWithMetadataEnvelope = {
+ /**
+ * Identifies the envelope's array output.
+ * @hidden
+ */
+ [envelopeDiscriminator]: 'array' as const,
+
+ /**
+ * Parses every record's detail using the supplied schema.
+ *
+ * @param data - the batch to parse
+ * @param schema - the schema for each record's detail
+ */
+ parse(data: unknown, schema: ZodType): T[] {
+ try {
+ return EventBridgeWithMetadataRecordSchema.extend({
+ Data: EventBridgeSchema.extend({ detail: schema }),
+ })
+ .array()
+ .nonempty()
+ .parse(data)
+ .map((record) => record.Data.detail);
+ } catch (error) {
+ throw new ParseError(
+ 'Failed to parse EventBridge WITH_METADATA envelope',
+ {
+ cause: error,
+ }
+ );
+ }
+ },
+
+ /**
+ * Parses a batch, returning the original input and error on failure.
+ *
+ * @param data - the batch to parse
+ * @param schema - the schema for each record's detail
+ */
+ safeParse(data: unknown, schema: ZodType): ParsedResult {
+ try {
+ return {
+ success: true,
+ data: EventBridgeWithMetadataEnvelope.parse(data, schema),
+ };
+ } catch (error) {
+ return {
+ success: false,
+ // parse always wraps validation and transform failures in ParseError.
+ error: error as ParseError,
+ originalEvent: data,
+ };
+ }
+ },
+};
+
+export { EventBridgeWithMetadataEnvelope };
diff --git a/packages/parser/src/envelopes/index.ts b/packages/parser/src/envelopes/index.ts
index 08e77b8756..d55201f5a6 100644
--- a/packages/parser/src/envelopes/index.ts
+++ b/packages/parser/src/envelopes/index.ts
@@ -3,6 +3,7 @@ export { ApiGatewayV2Envelope } from './api-gatewayv2.js';
export { CloudWatchEnvelope } from './cloudwatch.js';
export { DynamoDBStreamEnvelope } from './dynamodb.js';
export { EventBridgeEnvelope } from './eventbridge.js';
+export { EventBridgeWithMetadataEnvelope } from './eventbridge-with-metadata.js';
export { KafkaEnvelope } from './kafka.js';
export { KinesisEnvelope } from './kinesis.js';
export { KinesisFirehoseEnvelope } from './kinesis-firehose.js';
diff --git a/packages/parser/src/schemas/eventbridge-with-metadata.ts b/packages/parser/src/schemas/eventbridge-with-metadata.ts
new file mode 100644
index 0000000000..0e8857d8c8
--- /dev/null
+++ b/packages/parser/src/schemas/eventbridge-with-metadata.ts
@@ -0,0 +1,51 @@
+import { z } from 'zod';
+import { EventBridgeSchema } from './eventbridge.js';
+
+/**
+ * Validates system metadata for an EventBridge WITH_METADATA delivery.
+ *
+ * Only `aws:DeliveryType` is required. All metadata values, including sequence
+ * numbers that can exceed JavaScript's safe integer range, remain strings.
+ *
+ * @see https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-custom-bus-addressing.html
+ */
+const EventBridgeWithMetadataSystemMetadataSchema = z.object({
+ 'aws:DeliveryType': z.enum(['LIVE', 'REPLAY']),
+ ContentType: z.string().optional(),
+ EventGroupId: z.string().optional(),
+ DeduplicationId: z.string().optional(),
+ 'aws:EventId': z.string().optional(),
+ 'aws:IngestionTime': z.string().optional(),
+ 'aws:SequenceNumber': z.string().optional(),
+ 'aws:Source': z.string().optional(),
+ 'aws:DetailType': z.string().optional(),
+ 'aws:SchemaId': z.string().optional(),
+ 'aws:RegistryType': z.string().optional(),
+});
+
+/**
+ * Validates one WITH_METADATA record containing a standard EventBridge event.
+ *
+ * Use this schema directly for single-object delivery with `MaxBatchSize: 1`.
+ * Arbitrary `PutRawEvents` payloads are outside this schema's scope.
+ */
+const EventBridgeWithMetadataRecordSchema = z.object({
+ Data: EventBridgeSchema,
+ Metadata: z.record(z.string(), z.string()).optional(),
+ SystemMetadata: EventBridgeWithMetadataSystemMetadataSchema,
+});
+
+/**
+ * Validates a nonempty batch of WITH_METADATA records.
+ *
+ * Extend {@link EventBridgeWithMetadataRecordSchema | `EventBridgeWithMetadataRecordSchema`}
+ * before creating an array schema to validate application details and retain metadata.
+ */
+const EventBridgeWithMetadataSchema =
+ EventBridgeWithMetadataRecordSchema.array().nonempty();
+
+export {
+ EventBridgeWithMetadataRecordSchema,
+ EventBridgeWithMetadataSchema,
+ EventBridgeWithMetadataSystemMetadataSchema,
+};
diff --git a/packages/parser/src/schemas/index.ts b/packages/parser/src/schemas/index.ts
index 5e09159e9c..4c0219a29f 100644
--- a/packages/parser/src/schemas/index.ts
+++ b/packages/parser/src/schemas/index.ts
@@ -58,6 +58,11 @@ export {
DynamoDBStreamToKinesisRecord,
} from './dynamodb.js';
export { EventBridgeSchema } from './eventbridge.js';
+export {
+ EventBridgeWithMetadataRecordSchema,
+ EventBridgeWithMetadataSchema,
+ EventBridgeWithMetadataSystemMetadataSchema,
+} from './eventbridge-with-metadata.js';
export {
KafkaMskEventSchema,
KafkaRecordSchema,
diff --git a/packages/parser/src/types/index.ts b/packages/parser/src/types/index.ts
index dd4e56a842..5130910c65 100644
--- a/packages/parser/src/types/index.ts
+++ b/packages/parser/src/types/index.ts
@@ -39,6 +39,9 @@ export type {
DynamoDBStreamRecord,
DynamoDBStreamToKinesisRecordEvent,
EventBridgeEvent,
+ EventBridgeWithMetadataEvent,
+ EventBridgeWithMetadataRecord,
+ EventBridgeWithMetadataSystemMetadata,
KafkaMskEvent,
KafkaRecord,
KafkaSelfManagedEvent,
diff --git a/packages/parser/src/types/schema.ts b/packages/parser/src/types/schema.ts
index 9195e357dc..2c5fd98139 100644
--- a/packages/parser/src/types/schema.ts
+++ b/packages/parser/src/types/schema.ts
@@ -24,6 +24,9 @@ import type {
DynamoDBStreamSchema,
DynamoDBStreamToKinesisRecord,
EventBridgeSchema,
+ EventBridgeWithMetadataRecordSchema,
+ EventBridgeWithMetadataSchema,
+ EventBridgeWithMetadataSystemMetadataSchema,
KafkaMskEventSchema,
KafkaRecordSchema,
KafkaSelfManagedEventSchema,
@@ -122,6 +125,21 @@ type DynamoDBStreamToKinesisRecordEvent = z.infer<
type EventBridgeEvent = z.infer;
+/** Represents the system metadata of a WITH_METADATA delivery. */
+type EventBridgeWithMetadataSystemMetadata = z.infer<
+ typeof EventBridgeWithMetadataSystemMetadataSchema
+>;
+
+/** Represents one WITH_METADATA record containing a standard EventBridge event. */
+type EventBridgeWithMetadataRecord = z.infer<
+ typeof EventBridgeWithMetadataRecordSchema
+>;
+
+/** Represents a nonempty batch of WITH_METADATA records. */
+type EventBridgeWithMetadataEvent = z.infer<
+ typeof EventBridgeWithMetadataSchema
+>;
+
type KafkaSelfManagedEvent = z.infer;
type KafkaRecord = z.infer;
@@ -201,6 +219,9 @@ export type {
DynamoDBStreamRecord,
DynamoDBStreamToKinesisRecordEvent,
EventBridgeEvent,
+ EventBridgeWithMetadataEvent,
+ EventBridgeWithMetadataRecord,
+ EventBridgeWithMetadataSystemMetadata,
KafkaMskEvent,
KafkaRecord,
KafkaSelfManagedEvent,
diff --git a/packages/parser/tests/types/eventbridge-with-metadata.test-d.ts b/packages/parser/tests/types/eventbridge-with-metadata.test-d.ts
new file mode 100644
index 0000000000..9fd6284bee
--- /dev/null
+++ b/packages/parser/tests/types/eventbridge-with-metadata.test-d.ts
@@ -0,0 +1,63 @@
+import { describe, expectTypeOf, it } from 'vitest';
+import { z } from 'zod';
+import { EventBridgeWithMetadataEnvelope } from '../../src/envelopes/index.js';
+import { parse } from '../../src/parser.js';
+import type {
+ EventBridgeWithMetadataRecordSchema,
+ EventBridgeWithMetadataSchema,
+} from '../../src/schemas/index.js';
+import type {
+ EventBridgeWithMetadataEvent,
+ EventBridgeWithMetadataRecord,
+ EventBridgeWithMetadataSystemMetadata,
+ ParsedResult,
+} from '../../src/types/index.js';
+import type { ParserOutput } from '../../src/types/parser.js';
+
+describe('Types: EventBridge WITH_METADATA', () => {
+ it('infers transformed detail arrays for parsing and safe parsing', () => {
+ // Prepare
+ const schema = z.object({ orderId: z.string().transform(Number) });
+ const event: unknown = [];
+
+ // Act
+ const result = parse(event, EventBridgeWithMetadataEnvelope, schema);
+ const safeResult = parse(
+ event,
+ EventBridgeWithMetadataEnvelope,
+ schema,
+ true
+ );
+
+ // Assess
+ expectTypeOf(result).toEqualTypeOf<{ orderId: number }[]>();
+ expectTypeOf(safeResult).toEqualTypeOf<
+ ParsedResult
+ >();
+ expectTypeOf<
+ ParserOutput
+ >().toEqualTypeOf<{ orderId: number }[]>();
+ });
+
+ it('exports inferred record and batch types', () => {
+ // Assess
+ expectTypeOf<
+ z.infer
+ >().toEqualTypeOf();
+ expectTypeOf<
+ z.infer
+ >().toEqualTypeOf();
+ expectTypeOf().toEqualTypeOf<
+ EventBridgeWithMetadataRecord[]
+ >();
+ expectTypeOf().toEqualTypeOf<
+ Record | undefined
+ >();
+ expectTypeOf<
+ EventBridgeWithMetadataSystemMetadata['aws:SequenceNumber']
+ >().toEqualTypeOf();
+ expectTypeOf<
+ EventBridgeWithMetadataSystemMetadata['aws:DeliveryType']
+ >().toEqualTypeOf<'LIVE' | 'REPLAY'>();
+ });
+});
diff --git a/packages/parser/tests/unit/envelopes/eventbridge-with-metadata.test.ts b/packages/parser/tests/unit/envelopes/eventbridge-with-metadata.test.ts
new file mode 100644
index 0000000000..74a4169b26
--- /dev/null
+++ b/packages/parser/tests/unit/envelopes/eventbridge-with-metadata.test.ts
@@ -0,0 +1,158 @@
+import { describe, expect, it } from 'vitest';
+import { z } from 'zod';
+import { EventBridgeWithMetadataEnvelope } from '../../../src/envelopes/eventbridge-with-metadata.js';
+import { ParseError } from '../../../src/errors.js';
+import { parse } from '../../../src/parser.js';
+import { makeEventBridgeWithMetadataRecord } from '../helpers/utils.js';
+
+describe('Envelope: EventBridge WITH_METADATA', () => {
+ const schema = z.object({ orderId: z.string().transform(Number) });
+
+ it.each([1, 3])('parses and transforms %i records in order', (count) => {
+ // Prepare
+ const event = Array.from({ length: count }, (_, index) =>
+ makeEventBridgeWithMetadataRecord({ orderId: `${index}` })
+ );
+ const expected = Array.from({ length: count }, (_, index) => ({
+ orderId: index,
+ }));
+
+ // Act
+ const result = parse(event, EventBridgeWithMetadataEnvelope, schema);
+ const safeResult = parse(
+ event,
+ EventBridgeWithMetadataEnvelope,
+ schema,
+ true
+ );
+
+ // Assess
+ expect(result).toEqual(expected);
+ expect(safeResult).toEqual({ success: true, data: expected });
+ expect(event[0].Data.detail).toEqual({ orderId: '0' });
+ });
+
+ it.each([
+ {
+ name: 'invalid detail',
+ event: [
+ makeEventBridgeWithMetadataRecord({ orderId: '1' }),
+ makeEventBridgeWithMetadataRecord({ orderId: 2 }),
+ ],
+ path: [1, 'Data', 'detail', 'orderId'],
+ },
+ {
+ name: 'invalid metadata',
+ event: [
+ {
+ ...makeEventBridgeWithMetadataRecord({ orderId: '1' }),
+ SystemMetadata: {},
+ },
+ ],
+ path: [0, 'SystemMetadata', 'aws:DeliveryType'],
+ },
+ {
+ name: 'invalid producer metadata',
+ event: [
+ {
+ ...makeEventBridgeWithMetadataRecord({ orderId: '1' }),
+ Metadata: { tenant: 123 },
+ },
+ ],
+ path: [0, 'Metadata', 'tenant'],
+ },
+ {
+ name: 'invalid data',
+ event: [
+ { ...makeEventBridgeWithMetadataRecord({ orderId: '1' }), Data: null },
+ ],
+ path: [0, 'Data'],
+ },
+ { name: 'empty batch', event: [], path: [] },
+ {
+ name: 'single object',
+ event: makeEventBridgeWithMetadataRecord({ orderId: '1' }),
+ path: [],
+ },
+ ])(
+ 'fails the whole batch for $name and preserves the issue path',
+ ({ event, path }) => {
+ // Act
+ const result = EventBridgeWithMetadataEnvelope.safeParse(event, schema);
+
+ // Assess
+ expect(result).toEqual({
+ success: false,
+ originalEvent: event,
+ error: expect.objectContaining({
+ name: 'ParseError',
+ cause: expect.objectContaining({
+ issues: expect.arrayContaining([expect.objectContaining({ path })]),
+ }),
+ }),
+ });
+ if (!result.success) {
+ expect(result.originalEvent).toBe(event);
+ expect(result.error.cause).toBeInstanceOf(z.ZodError);
+ }
+ expect(() =>
+ EventBridgeWithMetadataEnvelope.parse(event, schema)
+ ).toThrow(ParseError);
+ expect(result).not.toHaveProperty('data');
+ }
+ );
+
+ it('retains validation errors for multiple invalid records', () => {
+ // Prepare
+ const event = [
+ makeEventBridgeWithMetadataRecord({}),
+ makeEventBridgeWithMetadataRecord({}),
+ ];
+
+ // Act
+ const result = EventBridgeWithMetadataEnvelope.safeParse(event, schema);
+
+ // Assess
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ const cause = result.error.cause;
+ expect(cause).toBeInstanceOf(z.ZodError);
+ if (cause instanceof z.ZodError) {
+ expect(cause.issues.map((issue) => issue.path)).toEqual([
+ [0, 'Data', 'detail', 'orderId'],
+ [1, 'Data', 'detail', 'orderId'],
+ ]);
+ }
+ }
+ });
+
+ it.each([new SyntaxError('invalid JSON'), 'transform failed'])(
+ 'preserves a thrown transform cause %s',
+ (cause) => {
+ // Prepare
+ const event = [makeEventBridgeWithMetadataRecord({ orderId: '1' })];
+ const throwingSchema = z.unknown().transform(() => {
+ throw cause;
+ });
+
+ // Act
+ const result = parse(
+ event,
+ EventBridgeWithMetadataEnvelope,
+ throwingSchema,
+ true
+ );
+
+ // Assess
+ expect(result.success).toBe(false);
+ if (!result.success) {
+ expect(result.error).toBeInstanceOf(ParseError);
+ expect(result.error.cause).toBe(cause);
+ expect(result.originalEvent).toBe(event);
+ }
+ expect(() =>
+ EventBridgeWithMetadataEnvelope.parse(event, throwingSchema)
+ ).toThrow(expect.objectContaining({ cause }));
+ }
+ );
+});
diff --git a/packages/parser/tests/unit/helpers/utils.ts b/packages/parser/tests/unit/helpers/utils.ts
index b319b78f99..e78ec11f43 100644
--- a/packages/parser/tests/unit/helpers/utils.ts
+++ b/packages/parser/tests/unit/helpers/utils.ts
@@ -1,5 +1,9 @@
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
+import type {
+ EventBridgeEvent,
+ EventBridgeWithMetadataRecord,
+} from '../../../src/types/index.js';
/**
* Reads and parses a JSON file from the specified events path and filename, returning the parsed object.
@@ -44,4 +48,29 @@ const omit = , Keys extends keyof T>(
return result;
};
-export { getTestEvent, omit };
+/**
+ * Creates a synthetic WITH_METADATA record from the classic EventBridge fixture.
+ *
+ * @param detail - the application payload
+ */
+const makeEventBridgeWithMetadataRecord = (
+ detail: unknown
+): EventBridgeWithMetadataRecord => ({
+ Data: {
+ ...getTestEvent({
+ eventsPath: 'eventbridge',
+ filename: 'base',
+ }),
+ detail,
+ },
+ Metadata: {},
+ SystemMetadata: {
+ 'aws:DeliveryType': 'LIVE',
+ 'aws:EventId': 'bus-event-id',
+ 'aws:SequenceNumber': '10000000000000003000',
+ EventGroupId: 'group-1',
+ DeduplicationId: 'dedup-1',
+ },
+});
+
+export { getTestEvent, makeEventBridgeWithMetadataRecord, omit };
diff --git a/packages/parser/tests/unit/parser.decorator.test.ts b/packages/parser/tests/unit/parser.decorator.test.ts
index 193f8e2340..b39fb9981f 100644
--- a/packages/parser/tests/unit/parser.decorator.test.ts
+++ b/packages/parser/tests/unit/parser.decorator.test.ts
@@ -3,11 +3,15 @@ import type { Context } from 'aws-lambda';
import { describe, expect, it, vi } from 'vitest';
import { z } from 'zod';
import { EventBridgeEnvelope } from '../../src/envelopes/eventbridge.js';
+import { EventBridgeWithMetadataEnvelope } from '../../src/envelopes/eventbridge-with-metadata.js';
import { ParseError } from '../../src/errors.js';
import { parser } from '../../src/index.js';
import { EventBridgeSchema } from '../../src/schemas/index.js';
import type { EventBridgeEvent, ParsedResult } from '../../src/types/index.js';
-import { getTestEvent } from './helpers/utils.js';
+import {
+ getTestEvent,
+ makeEventBridgeWithMetadataRecord,
+} from './helpers/utils.js';
describe('Decorator: parser', () => {
const schema = z.object({
@@ -368,4 +372,58 @@ describe('Decorator: parser', () => {
)
).toThrow(TypeError);
});
+
+ it('parses transformed WITH_METADATA details with the decorator', async () => {
+ // Prepare
+ const schema = z.object({ orderId: z.string().transform(Number) });
+ type Order = z.infer;
+ class Lambda implements LambdaInterface {
+ @parser({ schema, envelope: EventBridgeWithMetadataEnvelope })
+ public async handler(event: Order[], _context: Context) {
+ return event;
+ }
+ }
+ // Invoke through the Lambda boundary, which receives the unparsed event.
+ const lambda: LambdaInterface = new Lambda();
+ const event = [makeEventBridgeWithMetadataRecord({ orderId: '42' })];
+
+ // Act
+ const result = await lambda.handler(event, {} as Context, () => {});
+
+ // Assess
+ expect(result).toEqual([{ orderId: 42 }]);
+ });
+
+ it('returns a failed WITH_METADATA result for a throwing transform', async () => {
+ // Prepare
+ const cause = new SyntaxError('invalid JSON');
+ const throwingSchema = z.unknown().transform(() => {
+ throw cause;
+ });
+ class Lambda implements LambdaInterface {
+ @parser({
+ schema: throwingSchema,
+ envelope: EventBridgeWithMetadataEnvelope,
+ safeParse: true,
+ })
+ public async handler(
+ event: ParsedResult,
+ _context: Context
+ ) {
+ return event;
+ }
+ }
+ const lambda: LambdaInterface = new Lambda();
+ const event = [makeEventBridgeWithMetadataRecord({})];
+
+ // Act
+ const result = await lambda.handler(event, {} as Context, () => {});
+
+ // Assess
+ expect(result).toMatchObject({
+ success: false,
+ originalEvent: event,
+ error: { cause },
+ });
+ });
});
diff --git a/packages/parser/tests/unit/parser.middy.test.ts b/packages/parser/tests/unit/parser.middy.test.ts
index 0796a27b64..d050bab9e7 100644
--- a/packages/parser/tests/unit/parser.middy.test.ts
+++ b/packages/parser/tests/unit/parser.middy.test.ts
@@ -1,8 +1,9 @@
import middy from '@middy/core';
-import type { Context } from 'aws-lambda';
+import type { Context, Handler } from 'aws-lambda';
import { describe, expect, it, vi } from 'vitest';
import { z } from 'zod';
import { EventBridgeEnvelope } from '../../src/envelopes/eventbridge.js';
+import { EventBridgeWithMetadataEnvelope } from '../../src/envelopes/eventbridge-with-metadata.js';
import { SqsEnvelope } from '../../src/envelopes/sqs.js';
import { ParseError } from '../../src/errors.js';
import { parser } from '../../src/middleware/index.js';
@@ -11,7 +12,10 @@ import type {
ParsedResult,
SqsEvent,
} from '../../src/types/index.js';
-import { getTestEvent } from './helpers/utils.js';
+import {
+ getTestEvent,
+ makeEventBridgeWithMetadataRecord,
+} from './helpers/utils.js';
describe('Middleware: parser', () => {
const schema = z
@@ -407,4 +411,47 @@ describe('Middleware: parser', () => {
});
expect(otherOnError).toHaveBeenCalledTimes(1);
});
+
+ it('parses transformed WITH_METADATA details with middleware', async () => {
+ // Prepare
+ const schema = z.object({ orderId: z.string().transform(Number) });
+ const handler: Handler = middy()
+ .use(parser({ schema, envelope: EventBridgeWithMetadataEnvelope }))
+ .handler(async (event) => event);
+ const event = [makeEventBridgeWithMetadataRecord({ orderId: '42' })];
+
+ // Act
+ const result = await handler(event, {} as Context, () => {});
+
+ // Assess
+ expect(result).toEqual([{ orderId: 42 }]);
+ });
+
+ it('returns a failed WITH_METADATA result for a throwing transform', async () => {
+ // Prepare
+ const cause = new SyntaxError('invalid JSON');
+ const throwingSchema = z.unknown().transform(() => {
+ throw cause;
+ });
+ const handler: Handler = middy()
+ .use(
+ parser({
+ schema: throwingSchema,
+ envelope: EventBridgeWithMetadataEnvelope,
+ safeParse: true,
+ })
+ )
+ .handler(async (event) => event);
+ const event = [makeEventBridgeWithMetadataRecord({})];
+
+ // Act
+ const result = await handler(event, {} as Context, () => {});
+
+ // Assess
+ expect(result).toMatchObject({
+ success: false,
+ originalEvent: event,
+ error: { cause },
+ });
+ });
});
diff --git a/packages/parser/tests/unit/schema/eventbridge-with-metadata.test.ts b/packages/parser/tests/unit/schema/eventbridge-with-metadata.test.ts
new file mode 100644
index 0000000000..882fa8838a
--- /dev/null
+++ b/packages/parser/tests/unit/schema/eventbridge-with-metadata.test.ts
@@ -0,0 +1,210 @@
+import { describe, expect, it } from 'vitest';
+import { z } from 'zod';
+import { EventBridgeSchema } from '../../../src/schemas/eventbridge.js';
+import {
+ EventBridgeWithMetadataRecordSchema,
+ EventBridgeWithMetadataSchema,
+ EventBridgeWithMetadataSystemMetadataSchema,
+} from '../../../src/schemas/eventbridge-with-metadata.js';
+import { makeEventBridgeWithMetadataRecord, omit } from '../helpers/utils.js';
+
+describe('Schema: EventBridge WITH_METADATA', () => {
+ it.each([1, 3])('parses a batch of %i records in order', (count) => {
+ // Prepare
+ const event = Array.from({ length: count }, (_, index) =>
+ makeEventBridgeWithMetadataRecord({ orderId: `order-${index}` })
+ );
+
+ // Act
+ const result = EventBridgeWithMetadataSchema.parse(event);
+
+ // Assess
+ expect(result).toStrictEqual(event);
+ });
+
+ it.each(['900719925474099301', '10000000000000003000'])(
+ 'preserves sequence %s exactly',
+ (sequence) => {
+ // Prepare
+ const event = makeEventBridgeWithMetadataRecord({ orderId: '1' });
+ event.SystemMetadata['aws:SequenceNumber'] = sequence;
+
+ // Act
+ const result = EventBridgeWithMetadataRecordSchema.parse(event);
+
+ // Assess
+ expect(result.SystemMetadata['aws:SequenceNumber']).toBe(sequence);
+ expect(result.Data.id).not.toBe(result.SystemMetadata['aws:EventId']);
+ }
+ );
+
+ it.each(['LIVE', 'REPLAY'])(
+ 'accepts %s with only required system metadata',
+ (deliveryType) => {
+ // Prepare: synthetic omission case based on documented optionality.
+ const event = {
+ ...makeEventBridgeWithMetadataRecord({ orderId: '1' }),
+ SystemMetadata: { 'aws:DeliveryType': deliveryType },
+ };
+
+ // Act
+ const result = EventBridgeWithMetadataRecordSchema.parse(event);
+
+ // Assess
+ expect(result.SystemMetadata).toStrictEqual({
+ 'aws:DeliveryType': deliveryType,
+ });
+ }
+ );
+
+ it('retains replay names and independent event identifiers', () => {
+ // Prepare
+ const event = makeEventBridgeWithMetadataRecord({ orderId: '1' });
+ event.Data['replay-name'] = 'orders-replay';
+ event.SystemMetadata['aws:DeliveryType'] = 'REPLAY';
+
+ // Act
+ const result = EventBridgeWithMetadataRecordSchema.parse(event);
+
+ // Assess
+ expect(result).toStrictEqual(event);
+ });
+
+ it.each([undefined, {}, { tenant: 'example' }])(
+ 'accepts optional producer metadata %j',
+ (metadata) => {
+ // Prepare
+ const record = omit(
+ ['Metadata'],
+ makeEventBridgeWithMetadataRecord({ orderId: '1' })
+ );
+ const event =
+ metadata === undefined ? record : { ...record, Metadata: metadata };
+
+ // Act
+ const result = EventBridgeWithMetadataRecordSchema.parse(event);
+
+ // Assess
+ expect(result).toStrictEqual(event);
+ }
+ );
+
+ it.each([
+ [],
+ {},
+ null,
+ undefined,
+ 'event',
+ makeEventBridgeWithMetadataRecord({ orderId: '1' }),
+ ])('rejects a non-batch input %j', (event) => {
+ // Act
+ const result = EventBridgeWithMetadataSchema.safeParse(event);
+
+ // Assess
+ expect(result.success).toBe(false);
+ });
+
+ it.each([
+ undefined,
+ null,
+ [],
+ 'metadata',
+ {},
+ { 'aws:DeliveryType': 'OTHER' },
+ { 'aws:DeliveryType': 1 },
+ ])('rejects invalid system metadata %j', (systemMetadata) => {
+ // Prepare
+ const event = {
+ ...makeEventBridgeWithMetadataRecord({ orderId: '1' }),
+ SystemMetadata: systemMetadata,
+ };
+
+ // Act
+ const result = EventBridgeWithMetadataRecordSchema.safeParse(event);
+
+ // Assess
+ expect(result.success).toBe(false);
+ });
+
+ it.each([
+ 'ContentType',
+ 'EventGroupId',
+ 'DeduplicationId',
+ 'aws:EventId',
+ 'aws:IngestionTime',
+ 'aws:SequenceNumber',
+ 'aws:Source',
+ 'aws:DetailType',
+ 'aws:SchemaId',
+ 'aws:RegistryType',
+ ])('requires a string for optional field %s', (field) => {
+ // Prepare
+ const valid = { 'aws:DeliveryType': 'LIVE', [field]: 'value' };
+ const invalid = { ...valid, [field]: 123 };
+
+ // Act
+ const accepted = EventBridgeWithMetadataSystemMetadataSchema.parse(valid);
+ const rejected =
+ EventBridgeWithMetadataSystemMetadataSchema.safeParse(invalid);
+
+ // Assess
+ expect(accepted).toEqual(valid);
+ expect(rejected.success).toBe(false);
+ });
+
+ it.each([null, [], 'metadata', { tenant: 1 }])(
+ 'rejects invalid producer metadata %j',
+ (metadata) => {
+ // Prepare
+ const event = {
+ ...makeEventBridgeWithMetadataRecord({ orderId: '1' }),
+ Metadata: metadata,
+ };
+
+ // Act
+ const result = EventBridgeWithMetadataRecordSchema.safeParse(event);
+
+ // Assess
+ expect(result.success).toBe(false);
+ }
+ );
+
+ it.each([
+ null,
+ undefined,
+ { orderId: 'raw-event' },
+ { ...makeEventBridgeWithMetadataRecord({}).Data, time: 'invalid' },
+ omit(['detail-type'], makeEventBridgeWithMetadataRecord({}).Data),
+ ])('rejects malformed standard event data %j', (data) => {
+ // Prepare
+ const event = {
+ ...makeEventBridgeWithMetadataRecord({ orderId: '1' }),
+ Data: data,
+ };
+
+ // Act
+ const result = EventBridgeWithMetadataRecordSchema.safeParse(event);
+
+ // Assess
+ expect(result.success).toBe(false);
+ });
+
+ it('extends the record schema to parse details and retain metadata', () => {
+ // Prepare
+ const schema = EventBridgeWithMetadataRecordSchema.extend({
+ Data: EventBridgeSchema.extend({
+ detail: z.object({ orderId: z.string().transform(Number) }),
+ }),
+ });
+ const event = makeEventBridgeWithMetadataRecord({ orderId: '42' });
+ event.Metadata = { tenant: 'example' };
+
+ // Act
+ const result = schema.parse(event);
+
+ // Assess
+ expect(result.Data.detail).toEqual({ orderId: 42 });
+ expect(result.Metadata).toEqual({ tenant: 'example' });
+ expect(result.SystemMetadata).toEqual(event.SystemMetadata);
+ });
+});