diff --git a/lambda/shared/src/main/scala/feral/lambda/events/CloudWatchLogsEvent.scala b/lambda/shared/src/main/scala/feral/lambda/events/CloudWatchLogsEvent.scala new file mode 100644 index 00000000..5f59b9df --- /dev/null +++ b/lambda/shared/src/main/scala/feral/lambda/events/CloudWatchLogsEvent.scala @@ -0,0 +1,137 @@ +/* + * Copyright 2021 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package feral.lambda.events + +import io.circe.Decoder + +import java.time.Instant + +// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/aws-lambda/trigger/cloudwatch-logs.d.ts + +sealed abstract class CloudWatchLogsEvent { + def awslogs: CloudWatchLogsEventData +} + +object CloudWatchLogsEvent { + def apply(awslogs: CloudWatchLogsEventData): CloudWatchLogsEvent = + new Impl(awslogs) + + implicit val decoder: Decoder[CloudWatchLogsEvent] = + Decoder.forProduct1("awslogs")(CloudWatchLogsEvent.apply) + + private final case class Impl( + awslogs: CloudWatchLogsEventData + ) extends CloudWatchLogsEvent { + override def productPrefix = "CloudWatchLogsEvent" + } +} + +sealed abstract class CloudWatchLogsEventData { + def data: String +} + +object CloudWatchLogsEventData { + def apply(data: String): CloudWatchLogsEventData = + new Impl(data) + + private[events] implicit val decoder: Decoder[CloudWatchLogsEventData] = + Decoder.forProduct1("data")(CloudWatchLogsEventData.apply) + + private final case class Impl( + data: String + ) extends CloudWatchLogsEventData { + override def productPrefix = "CloudWatchLogsEventData" + } +} + +sealed abstract class CloudWatchLogsDecodedData { + def owner: String + def logGroup: String + def logStream: String + def subscriptionFilters: List[String] + def messageType: String + def logEvents: List[CloudWatchLogsLogEvent] +} + +object CloudWatchLogsDecodedData { + def apply( + owner: String, + logGroup: String, + logStream: String, + subscriptionFilters: List[String], + messageType: String, + logEvents: List[CloudWatchLogsLogEvent] + ): CloudWatchLogsDecodedData = + new Impl(owner, logGroup, logStream, subscriptionFilters, messageType, logEvents) + + implicit val decoder: Decoder[CloudWatchLogsDecodedData] = + Decoder.forProduct6( + "owner", + "logGroup", + "logStream", + "subscriptionFilters", + "messageType", + "logEvents" + )(CloudWatchLogsDecodedData.apply) + + private final case class Impl( + owner: String, + logGroup: String, + logStream: String, + subscriptionFilters: List[String], + messageType: String, + logEvents: List[CloudWatchLogsLogEvent] + ) extends CloudWatchLogsDecodedData { + override def productPrefix = "CloudWatchLogsDecodedData" + } +} + +sealed abstract class CloudWatchLogsLogEvent { + def id: String + def timestamp: Instant + def message: String + def extractedFields: Option[Map[String, String]] +} + +object CloudWatchLogsLogEvent { + import codecs.decodeInstant + + def apply( + id: String, + timestamp: Instant, + message: String, + extractedFields: Option[Map[String, String]] + ): CloudWatchLogsLogEvent = + new Impl(id, timestamp, message, extractedFields) + + private[events] implicit val decoder: Decoder[CloudWatchLogsLogEvent] = + Decoder.forProduct4( + "id", + "timestamp", + "message", + "extractedFields" + )(CloudWatchLogsLogEvent.apply) + + private final case class Impl( + id: String, + timestamp: Instant, + message: String, + extractedFields: Option[Map[String, String]] + ) extends CloudWatchLogsLogEvent { + override def productPrefix = "CloudWatchLogsLogEvent" + } +} diff --git a/lambda/shared/src/main/scala/feral/lambda/events/CodeCommitEvent.scala b/lambda/shared/src/main/scala/feral/lambda/events/CodeCommitEvent.scala new file mode 100644 index 00000000..3c52a668 --- /dev/null +++ b/lambda/shared/src/main/scala/feral/lambda/events/CodeCommitEvent.scala @@ -0,0 +1,180 @@ +/* + * Copyright 2021 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package feral.lambda.events + +import io.circe.Decoder + +// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/aws-lambda/trigger/codecommit.d.ts + +sealed abstract class CodeCommitEvent { + def records: List[CodeCommitRecord] +} + +object CodeCommitEvent { + def apply(records: List[CodeCommitRecord]): CodeCommitEvent = + new Impl(records) + + implicit val decoder: Decoder[CodeCommitEvent] = + Decoder.forProduct1("Records")(CodeCommitEvent.apply) + + private final case class Impl( + records: List[CodeCommitRecord] + ) extends CodeCommitEvent { + override def productPrefix = "CodeCommitEvent" + } +} + +sealed abstract class CodeCommitRecord { + def awsRegion: String + def codecommit: CodeCommitData + def customData: Option[String] + def eventId: String + def eventName: String + def eventPartNumber: Int + def eventSource: String + def eventSourceArn: String + def eventTime: String + def eventTotalParts: Int + def eventTriggerConfigId: String + def eventTriggerName: String + def eventVersion: String + def userIdentityArn: String +} + +object CodeCommitRecord { + def apply( + awsRegion: String, + codecommit: CodeCommitData, + customData: Option[String], + eventId: String, + eventName: String, + eventPartNumber: Int, + eventSource: String, + eventSourceArn: String, + eventTime: String, + eventTotalParts: Int, + eventTriggerConfigId: String, + eventTriggerName: String, + eventVersion: String, + userIdentityArn: String + ): CodeCommitRecord = + new Impl( + awsRegion, + codecommit, + customData, + eventId, + eventName, + eventPartNumber, + eventSource, + eventSourceArn, + eventTime, + eventTotalParts, + eventTriggerConfigId, + eventTriggerName, + eventVersion, + userIdentityArn + ) + + private[events] implicit val decoder: Decoder[CodeCommitRecord] = + Decoder.forProduct14( + "awsRegion", + "codecommit", + "customData", + "eventId", + "eventName", + "eventPartNumber", + "eventSource", + "eventSourceARN", + "eventTime", + "eventTotalParts", + "eventTriggerConfigId", + "eventTriggerName", + "eventVersion", + "userIdentityARN" + )(CodeCommitRecord.apply) + + private final case class Impl( + awsRegion: String, + codecommit: CodeCommitData, + customData: Option[String], + eventId: String, + eventName: String, + eventPartNumber: Int, + eventSource: String, + eventSourceArn: String, + eventTime: String, + eventTotalParts: Int, + eventTriggerConfigId: String, + eventTriggerName: String, + eventVersion: String, + userIdentityArn: String + ) extends CodeCommitRecord { + override def productPrefix = "CodeCommitRecord" + } +} + +sealed abstract class CodeCommitData { + def references: List[CodeCommitReference] +} + +object CodeCommitData { + def apply(references: List[CodeCommitReference]): CodeCommitData = + new Impl(references) + + private[events] implicit val decoder: Decoder[CodeCommitData] = + Decoder.forProduct1("references")(CodeCommitData.apply) + + private final case class Impl( + references: List[CodeCommitReference] + ) extends CodeCommitData { + override def productPrefix = "CodeCommitData" + } +} + +sealed abstract class CodeCommitReference { + def commit: String + def ref: String + def created: Option[Boolean] + def deleted: Option[Boolean] +} + +object CodeCommitReference { + def apply( + commit: String, + ref: String, + created: Option[Boolean], + deleted: Option[Boolean] + ): CodeCommitReference = + new Impl(commit, ref, created, deleted) + + private[events] implicit val decoder: Decoder[CodeCommitReference] = + Decoder.forProduct4( + "commit", + "ref", + "created", + "deleted" + )(CodeCommitReference.apply) + + private final case class Impl( + commit: String, + ref: String, + created: Option[Boolean], + deleted: Option[Boolean] + ) extends CodeCommitReference { + override def productPrefix = "CodeCommitReference" + } +} diff --git a/lambda/shared/src/main/scala/feral/lambda/events/SecretsManagerRotationEvent.scala b/lambda/shared/src/main/scala/feral/lambda/events/SecretsManagerRotationEvent.scala new file mode 100644 index 00000000..112fbfb7 --- /dev/null +++ b/lambda/shared/src/main/scala/feral/lambda/events/SecretsManagerRotationEvent.scala @@ -0,0 +1,68 @@ +/* + * Copyright 2021 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package feral.lambda.events + +import io.circe.Decoder + +// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/aws-lambda/trigger/secretsmanager.d.ts + +sealed abstract class SecretsManagerRotationEvent { + def step: SecretsManagerRotationEvent.Step + def secretId: String + def clientRequestToken: String +} + +object SecretsManagerRotationEvent { + + def apply( + step: Step, + secretId: String, + clientRequestToken: String + ): SecretsManagerRotationEvent = + new Impl(step, secretId, clientRequestToken) + + implicit val decoder: Decoder[SecretsManagerRotationEvent] = + Decoder.forProduct3( + "Step", + "SecretId", + "ClientRequestToken" + )(SecretsManagerRotationEvent.apply) + + private final case class Impl( + step: Step, + secretId: String, + clientRequestToken: String + ) extends SecretsManagerRotationEvent { + override def productPrefix = "SecretsManagerRotationEvent" + } + + sealed abstract class Step + object Step { + case object CreateSecret extends Step + case object SetSecret extends Step + case object TestSecret extends Step + case object FinishSecret extends Step + + implicit val decoder: Decoder[Step] = Decoder.decodeString.emap { + case "createSecret" => Right(CreateSecret) + case "setSecret" => Right(SetSecret) + case "testSecret" => Right(TestSecret) + case "finishSecret" => Right(FinishSecret) + case other => Left(s"Unknown rotation step: $other") + } + } +} diff --git a/lambda/shared/src/test/scala/feral/lambda/events/CloudWatchLogsEventSuite.scala b/lambda/shared/src/test/scala/feral/lambda/events/CloudWatchLogsEventSuite.scala new file mode 100644 index 00000000..4c1ba3fd --- /dev/null +++ b/lambda/shared/src/test/scala/feral/lambda/events/CloudWatchLogsEventSuite.scala @@ -0,0 +1,144 @@ +/* + * Copyright 2021 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package feral.lambda.events + +import io.circe.Json +import io.circe.literal._ +import munit.FunSuite + +import java.time.Instant + +class CloudWatchLogsEventSuite extends FunSuite { + import CloudWatchLogsEventSuite._ + + test("event decoder") { + assertEquals(event.as[CloudWatchLogsEvent].toTry.get, decodedEvent) + } + + test("decoded data decoder") { + assertEquals(decodedDataJson.as[CloudWatchLogsDecodedData].toTry.get, decodedData) + } + + test("decoded data decoder with extractedFields") { + assertEquals( + decodedDataWithFieldsJson.as[CloudWatchLogsDecodedData].toTry.get, + decodedDataWithFields) + } +} + +object CloudWatchLogsEventSuite { + + val decodedEvent: CloudWatchLogsEvent = + CloudWatchLogsEvent( + CloudWatchLogsEventData( + "H4sIAAAAAAAAE6tWKkktLlGyUlAqS8wpTgUAKLMMdBMAAAA=" + ) + ) + + val decodedData: CloudWatchLogsDecodedData = + CloudWatchLogsDecodedData( + "123456789012", + "testLogGroup", + "testLogStream", + List("testFilter"), + "DATA_MESSAGE", + List( + CloudWatchLogsLogEvent( + "eventId1", + Instant.ofEpochMilli(1440442987000L), + "[ERROR] First test message", + None + ), + CloudWatchLogsLogEvent( + "eventId2", + Instant.ofEpochMilli(1440442987001L), + "[ERROR] Second test message", + None + ) + ) + ) + + val decodedDataWithFields: CloudWatchLogsDecodedData = + CloudWatchLogsDecodedData( + "123456789012", + "testLogGroup", + "testLogStream", + List("testFilter"), + "DATA_MESSAGE", + List( + CloudWatchLogsLogEvent( + "eventId1", + Instant.ofEpochMilli(1440442987000L), + "[ERROR] First test message", + Some(Map("level" -> "ERROR", "component" -> "test")) + ) + ) + ) + + // https://docs.aws.amazon.com/lambda/latest/dg/services-cloudwatchlogs.html + val event: Json = json""" + { + "awslogs": { + "data": "H4sIAAAAAAAAE6tWKkktLlGyUlAqS8wpTgUAKLMMdBMAAAA=" + } + } + """ + + // https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/SubscriptionFilters.html + val decodedDataJson: Json = json""" + { + "owner": "123456789012", + "logGroup": "testLogGroup", + "logStream": "testLogStream", + "subscriptionFilters": ["testFilter"], + "messageType": "DATA_MESSAGE", + "logEvents": [ + { + "id": "eventId1", + "timestamp": 1440442987000, + "message": "[ERROR] First test message" + }, + { + "id": "eventId2", + "timestamp": 1440442987001, + "message": "[ERROR] Second test message" + } + ] + } + """ + + val decodedDataWithFieldsJson: Json = json""" + { + "owner": "123456789012", + "logGroup": "testLogGroup", + "logStream": "testLogStream", + "subscriptionFilters": ["testFilter"], + "messageType": "DATA_MESSAGE", + "logEvents": [ + { + "id": "eventId1", + "timestamp": 1440442987000, + "message": "[ERROR] First test message", + "extractedFields": { + "level": "ERROR", + "component": "test" + } + } + ] + } + """ +} diff --git a/lambda/shared/src/test/scala/feral/lambda/events/CodeCommitEventSuite.scala b/lambda/shared/src/test/scala/feral/lambda/events/CodeCommitEventSuite.scala new file mode 100644 index 00000000..e0aea537 --- /dev/null +++ b/lambda/shared/src/test/scala/feral/lambda/events/CodeCommitEventSuite.scala @@ -0,0 +1,159 @@ +/* + * Copyright 2021 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package feral.lambda.events + +import io.circe.Json +import io.circe.literal._ +import munit.FunSuite + +class CodeCommitEventSuite extends FunSuite { + import CodeCommitEventSuite._ + + test("decoder") { + assertEquals(event.as[CodeCommitEvent].toTry.get, decoded) + assertEquals( + eventNoCustomDataWithDelete.as[CodeCommitEvent].toTry.get, + decodedNoCustomDataWithDelete) + } +} + +object CodeCommitEventSuite { + + val decoded: CodeCommitEvent = + CodeCommitEvent( + List( + CodeCommitRecord( + "us-east-1", + CodeCommitData( + List( + CodeCommitReference( + "5e493c6f3067653f3d04eca608b4901eb227078", + "refs/heads/main", + Some(true), + None + ) + ) + ), + Some("this is custom data"), + "31eze9a2-1b2d-ffef-b1d1-e8ede6720eb3", + "TriggerEventTest", + 1, + "aws:codecommit", + "arn:aws:codecommit:us-east-1:123456789012:MyDemoRepo", + "2016-01-01T23:59:59.000+0000", + 1, + "5582e977-EXAMPLE", + "my-trigger", + "1", + "arn:aws:iam::123456789012:root" + ) + ) + ) + + val decodedNoCustomDataWithDelete: CodeCommitEvent = + CodeCommitEvent( + List( + CodeCommitRecord( + "us-east-1", + CodeCommitData( + List( + CodeCommitReference( + "a]b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2", + "refs/heads/feature", + None, + Some(true) + ) + ) + ), + None, + "42fze9a2-1b2d-ffef-b1d1-e8ede6720eb4", + "TriggerEventTest", + 1, + "aws:codecommit", + "arn:aws:codecommit:us-east-1:123456789012:MyDemoRepo", + "2016-02-01T23:59:59.000+0000", + 1, + "6693f088-EXAMPLE", + "my-trigger", + "1", + "arn:aws:iam::123456789012:root" + ) + ) + ) + + // https://docs.aws.amazon.com/lambda/latest/dg/services-codecommit.html + val event: Json = json""" + { + "Records": [ + { + "awsRegion": "us-east-1", + "codecommit": { + "references": [ + { + "commit": "5e493c6f3067653f3d04eca608b4901eb227078", + "ref": "refs/heads/main", + "created": true + } + ] + }, + "customData": "this is custom data", + "eventId": "31eze9a2-1b2d-ffef-b1d1-e8ede6720eb3", + "eventName": "TriggerEventTest", + "eventPartNumber": 1, + "eventSource": "aws:codecommit", + "eventSourceARN": "arn:aws:codecommit:us-east-1:123456789012:MyDemoRepo", + "eventTime": "2016-01-01T23:59:59.000+0000", + "eventTotalParts": 1, + "eventTriggerConfigId": "5582e977-EXAMPLE", + "eventTriggerName": "my-trigger", + "eventVersion": "1", + "userIdentityARN": "arn:aws:iam::123456789012:root" + } + ] + } + """ + + val eventNoCustomDataWithDelete: Json = json""" + { + "Records": [ + { + "awsRegion": "us-east-1", + "codecommit": { + "references": [ + { + "commit": "a]b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2", + "ref": "refs/heads/feature", + "deleted": true + } + ] + }, + "eventId": "42fze9a2-1b2d-ffef-b1d1-e8ede6720eb4", + "eventName": "TriggerEventTest", + "eventPartNumber": 1, + "eventSource": "aws:codecommit", + "eventSourceARN": "arn:aws:codecommit:us-east-1:123456789012:MyDemoRepo", + "eventTime": "2016-02-01T23:59:59.000+0000", + "eventTotalParts": 1, + "eventTriggerConfigId": "6693f088-EXAMPLE", + "eventTriggerName": "my-trigger", + "eventVersion": "1", + "userIdentityARN": "arn:aws:iam::123456789012:root" + } + ] + } + """ +} diff --git a/lambda/shared/src/test/scala/feral/lambda/events/SecretsManagerRotationEventSuite.scala b/lambda/shared/src/test/scala/feral/lambda/events/SecretsManagerRotationEventSuite.scala new file mode 100644 index 00000000..7de785e8 --- /dev/null +++ b/lambda/shared/src/test/scala/feral/lambda/events/SecretsManagerRotationEventSuite.scala @@ -0,0 +1,116 @@ +/* + * Copyright 2021 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package feral.lambda.events + +import io.circe.Json +import io.circe.literal._ +import munit.FunSuite + +class SecretsManagerRotationEventSuite extends FunSuite { + import SecretsManagerRotationEventSuite._ + + test("decoder") { + assertEquals( + createSecretEvent.as[SecretsManagerRotationEvent].toTry.get, + decodedCreateSecret) + assertEquals(setSecretEvent.as[SecretsManagerRotationEvent].toTry.get, decodedSetSecret) + assertEquals(testSecretEvent.as[SecretsManagerRotationEvent].toTry.get, decodedTestSecret) + assertEquals( + finishSecretEvent.as[SecretsManagerRotationEvent].toTry.get, + decodedFinishSecret) + } + + test("decoder unknown step") { + assert(unknownStepEvent.as[SecretsManagerRotationEvent].isLeft) + } +} + +object SecretsManagerRotationEventSuite { + + private val secretId = + "arn:aws:secretsmanager:us-east-1:123456789012:secret:MyTestSecret-a1b2c3" + private val token = "550e8400-e29b-41d4-a716-446655440000" + + val decodedCreateSecret: SecretsManagerRotationEvent = + SecretsManagerRotationEvent( + SecretsManagerRotationEvent.Step.CreateSecret, + secretId, + token + ) + + val decodedSetSecret: SecretsManagerRotationEvent = + SecretsManagerRotationEvent( + SecretsManagerRotationEvent.Step.SetSecret, + secretId, + token + ) + + val decodedTestSecret: SecretsManagerRotationEvent = + SecretsManagerRotationEvent( + SecretsManagerRotationEvent.Step.TestSecret, + secretId, + token + ) + + val decodedFinishSecret: SecretsManagerRotationEvent = + SecretsManagerRotationEvent( + SecretsManagerRotationEvent.Step.FinishSecret, + secretId, + token + ) + + // https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotating-secrets-lambda-function-overview.html + val createSecretEvent: Json = json""" + { + "Step": "createSecret", + "SecretId": "arn:aws:secretsmanager:us-east-1:123456789012:secret:MyTestSecret-a1b2c3", + "ClientRequestToken": "550e8400-e29b-41d4-a716-446655440000" + } + """ + + val setSecretEvent: Json = json""" + { + "Step": "setSecret", + "SecretId": "arn:aws:secretsmanager:us-east-1:123456789012:secret:MyTestSecret-a1b2c3", + "ClientRequestToken": "550e8400-e29b-41d4-a716-446655440000" + } + """ + + val testSecretEvent: Json = json""" + { + "Step": "testSecret", + "SecretId": "arn:aws:secretsmanager:us-east-1:123456789012:secret:MyTestSecret-a1b2c3", + "ClientRequestToken": "550e8400-e29b-41d4-a716-446655440000" + } + """ + + val finishSecretEvent: Json = json""" + { + "Step": "finishSecret", + "SecretId": "arn:aws:secretsmanager:us-east-1:123456789012:secret:MyTestSecret-a1b2c3", + "ClientRequestToken": "550e8400-e29b-41d4-a716-446655440000" + } + """ + + val unknownStepEvent: Json = json""" + { + "Step": "unknownStep", + "SecretId": "arn:aws:secretsmanager:us-east-1:123456789012:secret:MyTestSecret-a1b2c3", + "ClientRequestToken": "550e8400-e29b-41d4-a716-446655440000" + } + """ +}