diff --git a/.gitleaksignore b/.gitleaksignore index f6971326..bb8000c0 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -21,3 +21,5 @@ e12407e09151898bfd8d049d57eee9db9977d56b:.github/copilot-instructions.md:generic 82cf3b2e89ea24b97c4ffc09e618700fb1b0aff3:pact-contracts/pacts/letter-rendering/supplier-api-letter-request-prepared.json:generic-api-key:10 82f6be3e657b46d8447e77cdc1894fba0b855c26:tests/component-tests/testCases/create-letter-request.spec.ts:generic-api-key:10 debc75a97cfe551a69fd1e8694be483213322a9d:pact-contracts/pacts/letter-rendering/supplier-api-letter-request-prepared.json:generic-api-key:10 +777eb4047ad06b9e939a292ee18664a0ffee4f29:tests/resources/prepared-letter.json:generic-api-key:4 +4fa1923947bbff2387218d698d766cbb7c121a0f:pact-contracts/pacts/letter-rendering/supplier-api-letter-request-prepared.json:generic-api-key:10 diff --git a/infrastructure/terraform/components/api/README.md b/infrastructure/terraform/components/api/README.md index 202237b5..28d83bcc 100644 --- a/infrastructure/terraform/components/api/README.md +++ b/infrastructure/terraform/components/api/README.md @@ -38,6 +38,8 @@ No requirements. | Name | Source | Version | |------|--------|---------| +| [allocation\_lambda](#module\_allocation\_lambda) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-lambda.zip | n/a | +| [amendments\_queue](#module\_amendments\_queue) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-sqs.zip | n/a | | [authorizer\_lambda](#module\_authorizer\_lambda) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a | | [domain\_truststore](#module\_domain\_truststore) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-s3bucket.zip | n/a | | [eventpub](#module\_eventpub) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-eventpub.zip | n/a | @@ -48,14 +50,15 @@ No requirements. | [get\_status](#module\_get\_status) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a | | [kms](#module\_kms) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-kms.zip | n/a | | [letter\_status\_update](#module\_letter\_status\_update) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a | -| [letter\_status\_updates\_queue](#module\_letter\_status\_updates\_queue) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-sqs.zip | n/a | | [letter\_updates\_transformer](#module\_letter\_updates\_transformer) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a | | [logging\_bucket](#module\_logging\_bucket) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-s3bucket.zip | n/a | | [patch\_letter](#module\_patch\_letter) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a | | [post\_letters](#module\_post\_letters) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a | | [post\_mi](#module\_post\_mi) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a | | [s3bucket\_test\_letters](#module\_s3bucket\_test\_letters) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-s3bucket.zip | n/a | -| [sqs\_letter\_updates](#module\_sqs\_letter\_updates) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-sqs.zip | n/a | +| [supplier\_events\_forwarder\_lambda](#module\_supplier\_events\_forwarder\_lambda) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-lambda.zip | n/a | +| [supplier\_events\_queue](#module\_supplier\_events\_queue) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-sqs.zip | n/a | +| [supplier\_requests\_queue](#module\_supplier\_requests\_queue) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-sqs.zip | n/a | | [supplier\_ssl](#module\_supplier\_ssl) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-ssl.zip | n/a | | [upsert\_letter](#module\_upsert\_letter) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a | ## Outputs diff --git a/infrastructure/terraform/components/api/event_source_mapping_status_updates_to_handler.tf b/infrastructure/terraform/components/api/event_source_mapping_status_updates_to_handler.tf index ab3634c4..e9463793 100644 --- a/infrastructure/terraform/components/api/event_source_mapping_status_updates_to_handler.tf +++ b/infrastructure/terraform/components/api/event_source_mapping_status_updates_to_handler.tf @@ -1,12 +1,11 @@ resource "aws_lambda_event_source_mapping" "status_updates_sqs_to_status_update_handler" { - event_source_arn = module.letter_status_updates_queue.sqs_queue_arn + event_source_arn = module.supplier_requests_queue.sqs_queue_arn function_name = module.letter_status_update.function_arn batch_size = 10 - maximum_batching_window_in_seconds = 1 scaling_config { maximum_concurrency = 10 } depends_on = [ - module.letter_status_updates_queue, # ensures queue exists - module.letter_status_update # ensures update handler exists + module.supplier_requests_queue, # ensures queue exists + module.letter_status_update # ensures update handler exists ] } diff --git a/infrastructure/terraform/components/api/lambda_event_source_mapping_upsert_letter.tf b/infrastructure/terraform/components/api/lambda_event_source_mapping_upsert_letter.tf index a592ea9e..f4d6ad7f 100644 --- a/infrastructure/terraform/components/api/lambda_event_source_mapping_upsert_letter.tf +++ b/infrastructure/terraform/components/api/lambda_event_source_mapping_upsert_letter.tf @@ -1,8 +1,7 @@ resource "aws_lambda_event_source_mapping" "upsert_letter" { - event_source_arn = module.sqs_letter_updates.sqs_queue_arn + event_source_arn = module.amendments_queue.sqs_queue_arn function_name = module.upsert_letter.function_name batch_size = 10 - maximum_batching_window_in_seconds = 5 function_response_types = [ "ReportBatchItemFailures" ] diff --git a/infrastructure/terraform/components/api/module_lambda_allocation.tf b/infrastructure/terraform/components/api/module_lambda_allocation.tf new file mode 100644 index 00000000..4db8d4d6 --- /dev/null +++ b/infrastructure/terraform/components/api/module_lambda_allocation.tf @@ -0,0 +1,72 @@ +module "allocation_lambda" { + source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-lambda.zip" + + function_name = "allocate_supplier" + description = "Lambda function for allocating supplier" + + aws_account_id = var.aws_account_id + component = var.component + environment = var.environment + project = var.project + region = var.region + group = var.group + + log_retention_in_days = var.log_retention_in_days + kms_key_arn = module.kms.key_arn + + iam_policy_document = { + body = data.aws_iam_policy_document.allocation_lambda.json + } + + function_s3_bucket = local.acct.s3_buckets["lambda_function_artefacts"]["id"] + function_code_base_path = local.aws_lambda_functions_dir_path + function_code_dir = "allocation/dist" + function_include_common = true + handler_function_name = "handler" + runtime = "nodejs22.x" + memory = 128 + timeout = 29 + log_level = var.log_level + + force_lambda_code_deploy = var.force_lambda_code_deploy + enable_lambda_insights = false + + send_to_firehose = true + log_destination_arn = local.destination_arn + log_subscription_role_arn = local.acct.log_subscription_role_arn + + lambda_env_vars = { + QUEUE_URL = module.amendments_queue.sqs_queue_url + } +} + + +data "aws_iam_policy_document" "allocation_lambda" { + statement { + sid = "KMSPermissions" + effect = "Allow" + + actions = [ + "kms:Decrypt", + "kms:GenerateDataKey", + ] + + resources = [ + module.kms.key_arn, + ] + } + + statement { + sid = "AllowQueueAccess" + effect = "Allow" + + actions = [ + "sqs:SendMessage", + "sqs:GetQueueAttributes", + ] + + resources = [ + module.amendments_queue.sqs_queue_arn + ] + } +} diff --git a/infrastructure/terraform/components/api/module_lambda_letter_status_update.tf b/infrastructure/terraform/components/api/module_lambda_letter_status_update.tf index 59393bd2..d01b0c58 100644 --- a/infrastructure/terraform/components/api/module_lambda_letter_status_update.tf +++ b/infrastructure/terraform/components/api/module_lambda_letter_status_update.tf @@ -79,7 +79,7 @@ data "aws_iam_policy_document" "letter_status_update" { ] resources = [ - module.letter_status_updates_queue.sqs_queue_arn + module.supplier_requests_queue.sqs_queue_arn ] } } diff --git a/infrastructure/terraform/components/api/module_lambda_patch_letter.tf b/infrastructure/terraform/components/api/module_lambda_patch_letter.tf index b09c303f..41148490 100644 --- a/infrastructure/terraform/components/api/module_lambda_patch_letter.tf +++ b/infrastructure/terraform/components/api/module_lambda_patch_letter.tf @@ -35,7 +35,7 @@ module "patch_letter" { log_subscription_role_arn = local.acct.log_subscription_role_arn lambda_env_vars = merge(local.common_lambda_env_vars, { - QUEUE_URL = module.letter_status_updates_queue.sqs_queue_url + QUEUE_URL = module.supplier_requests_queue.sqs_queue_url }) } @@ -64,7 +64,7 @@ data "aws_iam_policy_document" "patch_letter_lambda" { ] resources = [ - module.letter_status_updates_queue.sqs_queue_arn + module.supplier_requests_queue.sqs_queue_arn ] } } diff --git a/infrastructure/terraform/components/api/module_lambda_post_letters.tf b/infrastructure/terraform/components/api/module_lambda_post_letters.tf index 8eb8ad52..5199cb34 100644 --- a/infrastructure/terraform/components/api/module_lambda_post_letters.tf +++ b/infrastructure/terraform/components/api/module_lambda_post_letters.tf @@ -35,7 +35,7 @@ module "post_letters" { log_subscription_role_arn = local.acct.log_subscription_role_arn lambda_env_vars = merge(local.common_lambda_env_vars, { - QUEUE_URL = module.letter_status_updates_queue.sqs_queue_url, + QUEUE_URL = module.supplier_requests_queue.sqs_queue_url, MAX_LIMIT = var.max_get_limit }) } @@ -65,7 +65,7 @@ data "aws_iam_policy_document" "post_letters" { ] resources = [ - module.letter_status_updates_queue.sqs_queue_arn + module.supplier_requests_queue.sqs_queue_arn ] } } diff --git a/infrastructure/terraform/components/api/module_lambda_supplier_events_forwarder.tf b/infrastructure/terraform/components/api/module_lambda_supplier_events_forwarder.tf new file mode 100644 index 00000000..524d67be --- /dev/null +++ b/infrastructure/terraform/components/api/module_lambda_supplier_events_forwarder.tf @@ -0,0 +1,86 @@ +module "supplier_events_forwarder_lambda" { + source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-lambda.zip" + + function_name = "supplier_events_forwarder" + description = "Lambda function for forwarding supplier events to Firehose" + + aws_account_id = var.aws_account_id + component = var.component + environment = var.environment + project = var.project + region = var.region + group = var.group + + log_retention_in_days = var.log_retention_in_days + kms_key_arn = module.kms.key_arn + + iam_policy_document = { + body = data.aws_iam_policy_document.supplier_events_forwarder_lambda.json + } + + function_s3_bucket = local.acct.s3_buckets["lambda_function_artefacts"]["id"] + function_code_base_path = local.aws_lambda_functions_dir_path + function_code_dir = "supplier-events-forwarder/dist" + function_include_common = true + handler_function_name = "handler" + runtime = "nodejs22.x" + memory = 128 + timeout = 29 + log_level = var.log_level + + force_lambda_code_deploy = var.force_lambda_code_deploy + enable_lambda_insights = false + + send_to_firehose = true + log_destination_arn = local.destination_arn + log_subscription_role_arn = local.acct.log_subscription_role_arn + + lambda_env_vars = { + FIREHOSE_DELIVERY_STREAM_NAME = module.eventsub.firehose_delivery_stream.name + } +} + +data "aws_iam_policy_document" "supplier_events_forwarder_lambda" { + statement { + sid = "KMSPermissions" + effect = "Allow" + + actions = [ + "kms:Decrypt", + "kms:GenerateDataKey", + ] + + resources = [ + module.kms.key_arn, + ] + } + + statement { + sid = "FirehosePermissions" + effect = "Allow" + + actions = [ + "firehose:PutRecord", + "firehose:PutRecordBatch", + ] + + resources = [ + module.eventsub.firehose_delivery_stream.arn, + ] + } + + statement { + sid = "SQSPermissions" + effect = "Allow" + + actions = [ + "sqs:ReceiveMessage", + "sqs:DeleteMessage", + "sqs:GetQueueAttributes", + ] + + resources = [ + module.supplier_events_queue.sqs_queue_arn, + ] + } +} diff --git a/infrastructure/terraform/components/api/module_lambda_upsert_letter.tf b/infrastructure/terraform/components/api/module_lambda_upsert_letter.tf index b4a4278b..15723ac3 100644 --- a/infrastructure/terraform/components/api/module_lambda_upsert_letter.tf +++ b/infrastructure/terraform/components/api/module_lambda_upsert_letter.tf @@ -77,7 +77,7 @@ data "aws_iam_policy_document" "upsert_letter_lambda" { ] resources = [ - module.sqs_letter_updates.sqs_queue_arn + module.amendments_queue.sqs_queue_arn ] } } diff --git a/infrastructure/terraform/components/api/module_sqs_amendments_queue.tf b/infrastructure/terraform/components/api/module_sqs_amendments_queue.tf new file mode 100644 index 00000000..ce9b6977 --- /dev/null +++ b/infrastructure/terraform/components/api/module_sqs_amendments_queue.tf @@ -0,0 +1,47 @@ +module "amendments_queue" { + source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-sqs.zip" + + aws_account_id = var.aws_account_id + component = var.component + environment = var.environment + project = var.project + region = var.region + name = "${local.csi}-amendments-queue" + + fifo_queue = true + content_based_deduplication = true + + sqs_kms_key_arn = module.kms.key_arn + + visibility_timeout_seconds = 60 + + create_dlq = true + sqs_policy_overload = data.aws_iam_policy_document.amendments_queue_policy.json +} + +data "aws_iam_policy_document" "amendments_queue_policy" { + version = "2012-10-17" + statement { + sid = "AllowSNSToSendMessage" + effect = "Allow" + + principals { + type = "Service" + identifiers = ["sns.amazonaws.com"] + } + + actions = [ + "sqs:SendMessage" + ] + + resources = [ + "arn:aws:sqs:${var.region}:${var.aws_account_id}:${local.csi}-amendments-queue.fifo" + ] + + condition { + test = "ArnEquals" + variable = "aws:SourceArn" + values = [module.eventsub.sns_topic_supplier.arn] + } + } +} diff --git a/infrastructure/terraform/components/api/module_sqs_letter_updates.tf b/infrastructure/terraform/components/api/module_sqs_letter_updates.tf deleted file mode 100644 index 472afb81..00000000 --- a/infrastructure/terraform/components/api/module_sqs_letter_updates.tf +++ /dev/null @@ -1,71 +0,0 @@ -module "sqs_letter_updates" { - source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-sqs.zip" - - aws_account_id = var.aws_account_id - component = var.component - environment = var.environment - project = var.project - region = var.region - name = "letter-updates" - - sqs_kms_key_arn = module.kms.key_arn - - visibility_timeout_seconds = 60 - - create_dlq = true - sqs_policy_overload = data.aws_iam_policy_document.letter_updates_queue_policy.json -} - -data "aws_iam_policy_document" "letter_updates_queue_policy" { - version = "2012-10-17" - statement { - sid = "AllowSNSToSendMessage" - effect = "Allow" - - principals { - type = "Service" - identifiers = ["sns.amazonaws.com"] - } - - actions = [ - "sqs:SendMessage" - ] - - resources = [ - "arn:aws:sqs:${var.region}:${var.aws_account_id}:${var.project}-${var.environment}-${var.component}-letter-updates-queue" - ] - - condition { - test = "ArnEquals" - variable = "aws:SourceArn" - values = [module.eventsub.sns_topic.arn] - } - } - - statement { - sid = "AllowSNSPermissions" - effect = "Allow" - - principals { - type = "Service" - identifiers = ["sns.amazonaws.com"] - } - - actions = [ - "sqs:SendMessage", - "sqs:ListQueueTags", - "sqs:GetQueueUrl", - "sqs:GetQueueAttributes", - ] - - resources = [ - "arn:aws:sqs:${var.region}:${var.aws_account_id}:${var.project}-${var.environment}-${var.component}-letter-updates-queue" - ] - - condition { - test = "ArnEquals" - variable = "aws:SourceArn" - values = [module.eventsub.sns_topic.arn] - } - } -} diff --git a/infrastructure/terraform/components/api/module_sqs_supplier_events_queue.tf b/infrastructure/terraform/components/api/module_sqs_supplier_events_queue.tf new file mode 100644 index 00000000..c0255d46 --- /dev/null +++ b/infrastructure/terraform/components/api/module_sqs_supplier_events_queue.tf @@ -0,0 +1,47 @@ +module "supplier_events_queue" { + source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-sqs.zip" + + aws_account_id = var.aws_account_id + component = var.component + environment = var.environment + project = var.project + region = var.region + name = "${local.csi}-supplier-events-queue" + + fifo_queue = true + content_based_deduplication = true + + sqs_kms_key_arn = module.kms.key_arn + + visibility_timeout_seconds = 60 + + create_dlq = true + sqs_policy_overload = data.aws_iam_policy_document.supplier_events_queue_policy.json +} + +data "aws_iam_policy_document" "supplier_events_queue_policy" { + version = "2012-10-17" + statement { + sid = "AllowSNSToSendMessage" + effect = "Allow" + + principals { + type = "Service" + identifiers = ["sns.amazonaws.com"] + } + + actions = [ + "sqs:SendMessage" + ] + + resources = [ + "arn:aws:sqs:${var.region}:${var.aws_account_id}:${local.csi}-supplier-events-queue.fifo" + ] + + condition { + test = "ArnEquals" + variable = "aws:SourceArn" + values = [module.eventsub.sns_topic_supplier.arn] + } + } +} diff --git a/infrastructure/terraform/components/api/module_sqs_letter_status_updates.tf b/infrastructure/terraform/components/api/module_sqs_supplier_requests_queue.tf similarity index 71% rename from infrastructure/terraform/components/api/module_sqs_letter_status_updates.tf rename to infrastructure/terraform/components/api/module_sqs_supplier_requests_queue.tf index a604faaf..0fbdc1bc 100644 --- a/infrastructure/terraform/components/api/module_sqs_letter_status_updates.tf +++ b/infrastructure/terraform/components/api/module_sqs_supplier_requests_queue.tf @@ -1,8 +1,8 @@ # Queue to transport update letter status messages -module "letter_status_updates_queue" { +module "supplier_requests_queue" { source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.24/terraform-sqs.zip" - name = "letter_status_updates_queue" + name = "${local.csi}-supplier-requests-queue" aws_account_id = var.aws_account_id component = var.component @@ -10,6 +10,9 @@ module "letter_status_updates_queue" { project = var.project region = var.region + fifo_queue = true + content_based_deduplication = true + sqs_kms_key_arn = module.kms.key_arn create_dlq = true diff --git a/infrastructure/terraform/components/api/sns_topic_subscription_allocation_lambda.tf b/infrastructure/terraform/components/api/sns_topic_subscription_allocation_lambda.tf new file mode 100644 index 00000000..91fc7ae4 --- /dev/null +++ b/infrastructure/terraform/components/api/sns_topic_subscription_allocation_lambda.tf @@ -0,0 +1,13 @@ +resource "aws_sns_topic_subscription" "allocation_lambda" { + topic_arn = module.eventsub.sns_topic_event_bus.arn + protocol = "lambda" + endpoint = module.allocation_lambda.function_arn +} + +resource "aws_lambda_permission" "allocation_lambda_sns" { + statement_id = "AllowExecutionFromSNS" + action = "lambda:InvokeFunction" + function_name = module.allocation_lambda.function_name + principal = "sns.amazonaws.com" + source_arn = module.eventsub.sns_topic_event_bus.arn +} diff --git a/infrastructure/terraform/components/api/sns_topic_subscription_amendments_queue.tf b/infrastructure/terraform/components/api/sns_topic_subscription_amendments_queue.tf new file mode 100644 index 00000000..e609684d --- /dev/null +++ b/infrastructure/terraform/components/api/sns_topic_subscription_amendments_queue.tf @@ -0,0 +1,6 @@ +resource "aws_sns_topic_subscription" "amendments_queue" { + topic_arn = module.eventsub.sns_topic_supplier.arn + protocol = "sqs" + endpoint = module.amendments_queue.sqs_queue_arn + raw_message_delivery = false +} diff --git a/infrastructure/terraform/components/api/sns_topic_subscription_eventsub_sqs_letter_updates.tf b/infrastructure/terraform/components/api/sns_topic_subscription_eventsub_sqs_letter_updates.tf deleted file mode 100644 index 9c232c14..00000000 --- a/infrastructure/terraform/components/api/sns_topic_subscription_eventsub_sqs_letter_updates.tf +++ /dev/null @@ -1,5 +0,0 @@ -resource "aws_sns_topic_subscription" "eventsub_sqs_letter_updates" { - topic_arn = module.eventsub.sns_topic.arn - protocol = "sqs" - endpoint = module.sqs_letter_updates.sqs_queue_arn -} diff --git a/infrastructure/terraform/components/api/sns_topic_subscription_supplier_events_forwarder_lambda.tf b/infrastructure/terraform/components/api/sns_topic_subscription_supplier_events_forwarder_lambda.tf new file mode 100644 index 00000000..e8e3c01d --- /dev/null +++ b/infrastructure/terraform/components/api/sns_topic_subscription_supplier_events_forwarder_lambda.tf @@ -0,0 +1,18 @@ +resource "aws_sns_topic_subscription" "supplier_events_queue" { + topic_arn = module.eventsub.sns_topic_supplier.arn + protocol = "sqs" + endpoint = module.supplier_events_queue.sqs_queue_arn + raw_message_delivery = false +} + +resource "aws_lambda_event_source_mapping" "supplier_events_forwarder" { + event_source_arn = module.supplier_events_queue.sqs_queue_arn + function_name = module.supplier_events_forwarder_lambda.function_arn + batch_size = 10 + scaling_config { maximum_concurrency = 10 } + + depends_on = [ + module.supplier_events_queue, + module.supplier_events_forwarder_lambda + ] +} diff --git a/infrastructure/terraform/modules/eventsub/README.md b/infrastructure/terraform/modules/eventsub/README.md index a5653fda..ea09ed78 100644 --- a/infrastructure/terraform/modules/eventsub/README.md +++ b/infrastructure/terraform/modules/eventsub/README.md @@ -39,8 +39,10 @@ | Name | Description | |------|-------------| +| [firehose\_delivery\_stream](#output\_firehose\_delivery\_stream) | Kinesis Firehose Delivery Stream ARN and Name | | [s3\_bucket\_event\_cache](#output\_s3\_bucket\_event\_cache) | S3 Bucket ARN and Name for event cache | -| [sns\_topic](#output\_sns\_topic) | SNS Topic ARN and Name | +| [sns\_topic\_event\_bus](#output\_sns\_topic\_event\_bus) | SNS Topic ARN and Name | +| [sns\_topic\_supplier](#output\_sns\_topic\_supplier) | SNS Topic ARN and Name | diff --git a/infrastructure/terraform/modules/eventsub/cloudwatch_metric_alarm_sns_delivery_failures.tf b/infrastructure/terraform/modules/eventsub/cloudwatch_metric_alarm_sns_delivery_failures.tf index e8ef1249..f174026f 100644 --- a/infrastructure/terraform/modules/eventsub/cloudwatch_metric_alarm_sns_delivery_failures.tf +++ b/infrastructure/terraform/modules/eventsub/cloudwatch_metric_alarm_sns_delivery_failures.tf @@ -11,6 +11,6 @@ resource "aws_cloudwatch_metric_alarm" "sns_delivery_failures" { treat_missing_data = "notBreaching" dimensions = { - TopicName = aws_sns_topic.main.name + TopicName = aws_sns_topic.sns_topic_event_bus.name } } diff --git a/infrastructure/terraform/modules/eventsub/outputs.tf b/infrastructure/terraform/modules/eventsub/outputs.tf index e2ff3b38..6ddc8ef0 100644 --- a/infrastructure/terraform/modules/eventsub/outputs.tf +++ b/infrastructure/terraform/modules/eventsub/outputs.tf @@ -1,11 +1,27 @@ -output "sns_topic" { +output "sns_topic_event_bus" { description = "SNS Topic ARN and Name" value = { - arn = aws_sns_topic.main.arn - name = aws_sns_topic.main.name + arn = aws_sns_topic.sns_topic_event_bus.arn + name = aws_sns_topic.sns_topic_event_bus.name } } +output "sns_topic_supplier" { + description = "SNS Topic ARN and Name" + value = { + arn = aws_sns_topic.sns_topic_supplier.arn + name = aws_sns_topic.sns_topic_supplier.name + } +} + +output "firehose_delivery_stream" { + description = "Kinesis Firehose Delivery Stream ARN and Name" + value = var.enable_event_cache ? { + arn = aws_kinesis_firehose_delivery_stream.main[0].arn + name = aws_kinesis_firehose_delivery_stream.main[0].name + } : {} +} + output "s3_bucket_event_cache" { description = "S3 Bucket ARN and Name for event cache" value = var.enable_event_cache ? { diff --git a/infrastructure/terraform/modules/eventsub/sns_topic.tf b/infrastructure/terraform/modules/eventsub/sns_topic_event_bus.tf similarity index 95% rename from infrastructure/terraform/modules/eventsub/sns_topic.tf rename to infrastructure/terraform/modules/eventsub/sns_topic_event_bus.tf index cc30db15..28299fe7 100644 --- a/infrastructure/terraform/modules/eventsub/sns_topic.tf +++ b/infrastructure/terraform/modules/eventsub/sns_topic_event_bus.tf @@ -1,5 +1,5 @@ -resource "aws_sns_topic" "main" { - name = local.csi +resource "aws_sns_topic" "sns_topic_event_bus" { + name = "${local.csi}-event-bus-events" kms_master_key_id = var.kms_key_arn application_failure_feedback_role_arn = var.enable_sns_delivery_logging == true ? aws_iam_role.sns_delivery_logging_role[0].arn : null diff --git a/infrastructure/terraform/modules/eventsub/sns_topic_policy.tf b/infrastructure/terraform/modules/eventsub/sns_topic_policy.tf index a772e9e7..0a396baf 100644 --- a/infrastructure/terraform/modules/eventsub/sns_topic_policy.tf +++ b/infrastructure/terraform/modules/eventsub/sns_topic_policy.tf @@ -1,14 +1,78 @@ -resource "aws_sns_topic_policy" "main" { - arn = aws_sns_topic.main.arn +resource "aws_sns_topic_policy" "sns_topic_event_bus" { + arn = aws_sns_topic.sns_topic_event_bus.arn - policy = data.aws_iam_policy_document.sns_topic_policy.json + policy = data.aws_iam_policy_document.sns_topic_event_bus_policy.json } -data "aws_iam_policy_document" "sns_topic_policy" { +resource "aws_sns_topic_policy" "sns_topic_supplier" { + arn = aws_sns_topic.sns_topic_supplier.arn + + policy = data.aws_iam_policy_document.sns_topic_supplier_policy.json +} + +data "aws_iam_policy_document" "sns_topic_event_bus_policy" { + policy_id = "__default_policy_ID" + + statement { + sid = "AllowAllSNSActionsFromAccount" + effect = "Allow" + + principals { + type = "AWS" + identifiers = ["*"] + } + + actions = [ + "SNS:Subscribe", + "SNS:SetTopicAttributes", + "SNS:RemovePermission", + "SNS:Receive", + "SNS:Publish", + "SNS:ListSubscriptionsByTopic", + "SNS:GetTopicAttributes", + "SNS:DeleteTopic", + "SNS:AddPermission", + ] + + resources = [ + aws_sns_topic.sns_topic_event_bus.arn, + ] + + condition { + test = "StringEquals" + variable = "AWS:SourceOwner" + + values = [ + var.aws_account_id, + ] + } + } + + statement { + sid = "AllowAllSNSActionsFromSharedAccount" + effect = "Allow" + actions = [ + "SNS:Publish", + ] + + principals { + type = "AWS" + identifiers = [ + "arn:aws:iam::${var.shared_infra_account_id}:root" + ] + } + + resources = [ + aws_sns_topic.sns_topic_event_bus.arn, + ] + } +} + +data "aws_iam_policy_document" "sns_topic_supplier_policy" { policy_id = "__default_policy_ID" statement { - sid = "AllowAllSNSActionsFromAccount" + sid = "AllowAllSNSActionsFromAccount" effect = "Allow" principals { @@ -29,7 +93,7 @@ data "aws_iam_policy_document" "sns_topic_policy" { ] resources = [ - aws_sns_topic.main.arn, + aws_sns_topic.sns_topic_supplier.arn, ] condition { @@ -43,7 +107,7 @@ data "aws_iam_policy_document" "sns_topic_policy" { } statement { - sid = "AllowAllSNSActionsFromSharedAccount" + sid = "AllowAllSNSActionsFromSharedAccount" effect = "Allow" actions = [ "SNS:Publish", @@ -57,7 +121,7 @@ data "aws_iam_policy_document" "sns_topic_policy" { } resources = [ - aws_sns_topic.main.arn, + aws_sns_topic.sns_topic_supplier.arn, ] } } diff --git a/infrastructure/terraform/modules/eventsub/sns_topic_subscription_firehose.tf b/infrastructure/terraform/modules/eventsub/sns_topic_subscription_firehose.tf index 42457f6d..120f5582 100644 --- a/infrastructure/terraform/modules/eventsub/sns_topic_subscription_firehose.tf +++ b/infrastructure/terraform/modules/eventsub/sns_topic_subscription_firehose.tf @@ -1,7 +1,7 @@ -resource "aws_sns_topic_subscription" "firehose" { +resource "aws_sns_topic_subscription" "sns_topic_event_bus_firehose" { count = var.enable_event_cache ? 1 : 0 - topic_arn = aws_sns_topic.main.arn + topic_arn = aws_sns_topic.sns_topic_event_bus.arn protocol = "firehose" subscription_role_arn = aws_iam_role.sns_role.arn endpoint = aws_kinesis_firehose_delivery_stream.main[0].arn diff --git a/infrastructure/terraform/modules/eventsub/sns_topic_supplier.tf b/infrastructure/terraform/modules/eventsub/sns_topic_supplier.tf new file mode 100644 index 00000000..31bb9d77 --- /dev/null +++ b/infrastructure/terraform/modules/eventsub/sns_topic_supplier.tf @@ -0,0 +1,27 @@ +resource "aws_sns_topic" "sns_topic_supplier" { + name = "${local.csi}-supplier-events.fifo" + kms_master_key_id = var.kms_key_arn + + fifo_topic = true + content_based_deduplication = true + + application_failure_feedback_role_arn = var.enable_sns_delivery_logging == true ? aws_iam_role.sns_delivery_logging_role[0].arn : null + application_success_feedback_role_arn = var.enable_sns_delivery_logging == true ? aws_iam_role.sns_delivery_logging_role[0].arn : null + application_success_feedback_sample_rate = var.enable_sns_delivery_logging == true ? var.sns_success_logging_sample_percent : null + + firehose_failure_feedback_role_arn = var.enable_sns_delivery_logging == true ? aws_iam_role.sns_delivery_logging_role[0].arn : null + firehose_success_feedback_role_arn = var.enable_sns_delivery_logging == true ? aws_iam_role.sns_delivery_logging_role[0].arn : null + firehose_success_feedback_sample_rate = var.enable_sns_delivery_logging == true ? var.sns_success_logging_sample_percent : null + + http_failure_feedback_role_arn = var.enable_sns_delivery_logging == true ? aws_iam_role.sns_delivery_logging_role[0].arn : null + http_success_feedback_role_arn = var.enable_sns_delivery_logging == true ? aws_iam_role.sns_delivery_logging_role[0].arn : null + http_success_feedback_sample_rate = var.enable_sns_delivery_logging == true ? var.sns_success_logging_sample_percent : null + + lambda_failure_feedback_role_arn = var.enable_sns_delivery_logging == true ? aws_iam_role.sns_delivery_logging_role[0].arn : null + lambda_success_feedback_role_arn = var.enable_sns_delivery_logging == true ? aws_iam_role.sns_delivery_logging_role[0].arn : null + lambda_success_feedback_sample_rate = var.enable_sns_delivery_logging == true ? var.sns_success_logging_sample_percent : null + + sqs_failure_feedback_role_arn = var.enable_sns_delivery_logging == true ? aws_iam_role.sns_delivery_logging_role[0].arn : null + sqs_success_feedback_role_arn = var.enable_sns_delivery_logging == true ? aws_iam_role.sns_delivery_logging_role[0].arn : null + sqs_success_feedback_sample_rate = var.enable_sns_delivery_logging == true ? var.sns_success_logging_sample_percent : null +} diff --git a/internal/datastore/src/types.md b/internal/datastore/src/types.md index 89056843..70504c55 100644 --- a/internal/datastore/src/types.md +++ b/internal/datastore/src/types.md @@ -22,6 +22,9 @@ erDiagram string supplierStatus string supplierStatusSk number ttl "min: -9007199254740991, max: 9007199254740991" + string source + string subject + string billingRef } ``` diff --git a/internal/events/package.json b/internal/events/package.json index 09e72bde..395859af 100644 --- a/internal/events/package.json +++ b/internal/events/package.json @@ -50,5 +50,5 @@ "typecheck": "tsc --noEmit" }, "types": "dist/index.d.ts", - "version": "1.0.6" + "version": "1.0.7" } diff --git a/lambdas/allocation/.eslintignore b/lambdas/allocation/.eslintignore new file mode 100644 index 00000000..1521c8b7 --- /dev/null +++ b/lambdas/allocation/.eslintignore @@ -0,0 +1 @@ +dist diff --git a/lambdas/allocation/.gitignore b/lambdas/allocation/.gitignore new file mode 100644 index 00000000..9b19292a --- /dev/null +++ b/lambdas/allocation/.gitignore @@ -0,0 +1,4 @@ +.build +coverage +node_modules +dist diff --git a/lambdas/allocation/jest.config.ts b/lambdas/allocation/jest.config.ts new file mode 100644 index 00000000..f88e7277 --- /dev/null +++ b/lambdas/allocation/jest.config.ts @@ -0,0 +1,60 @@ +import type { Config } from "jest"; + +export const baseJestConfig: Config = { + preset: "ts-jest", + + // Automatically clear mock calls, instances, contexts and results before every test + clearMocks: true, + + // Indicates whether the coverage information should be collected while executing the test + collectCoverage: true, + + // The directory where Jest should output its coverage files + coverageDirectory: "./.reports/unit/coverage", + + // Indicates which provider should be used to instrument code for coverage + coverageProvider: "babel", + + coverageThreshold: { + global: { + branches: 100, + functions: 100, + lines: 100, + statements: -10, + }, + }, + + coveragePathIgnorePatterns: ["/__tests__/"], + transform: { "^.+\\.ts$": "ts-jest" }, + testPathIgnorePatterns: [".build"], + testMatch: ["**/?(*.)+(spec|test).[jt]s?(x)"], + + // Use this configuration option to add custom reporters to Jest + reporters: [ + "default", + [ + "jest-html-reporter", + { + pageTitle: "Test Report", + outputPath: "./.reports/unit/test-report.html", + includeFailureMsg: true, + }, + ], + ], + + // The test environment that will be used for testing + testEnvironment: "jsdom", +}; + +const utilsJestConfig = { + ...baseJestConfig, + + testEnvironment: "node", + + coveragePathIgnorePatterns: [ + ...(baseJestConfig.coveragePathIgnorePatterns ?? []), + "zod-validators.ts", + ], +}; + +export default utilsJestConfig; diff --git a/lambdas/allocation/package.json b/lambdas/allocation/package.json new file mode 100644 index 00000000..4c9b09e8 --- /dev/null +++ b/lambdas/allocation/package.json @@ -0,0 +1,27 @@ +{ + "dependencies": { + "@aws-sdk/client-sqs": "^3.925.0", + "@nhsdigital/nhs-notify-event-schemas-supplier-api": "*", + "aws-lambda": "^1.0.7", + "esbuild": "^0.25.11", + "pino": "^9.7.0", + "zod": "^4.1.11" + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.2", + "@types/jest": "^30.0.0", + "jest": "^30.2.0", + "jest-mock-extended": "^4.0.0", + "typescript": "^5.9.3" + }, + "name": "nhs-notify-supplier-allocation", + "private": true, + "scripts": { + "lambda-build": "rm -rf dist && npx esbuild --bundle --minify --sourcemap --target=es2020 --platform=node --loader:.node=file --entry-names=[name] --outdir=dist src/index.ts", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "test:unit": "jest", + "typecheck": "tsc --noEmit" + }, + "version": "0.0.1" +} diff --git a/lambdas/allocation/src/__tests__/allocator.test.ts b/lambdas/allocation/src/__tests__/allocator.test.ts new file mode 100644 index 00000000..619e7f02 --- /dev/null +++ b/lambdas/allocation/src/__tests__/allocator.test.ts @@ -0,0 +1,159 @@ +import { Context, SNSEvent, SNSEventRecord } from "aws-lambda"; +import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs"; +import { mockDeep } from "jest-mock-extended"; +import pino from "pino"; +import { + $LetterEvent, + LetterEvent, +} from "@nhsdigital/nhs-notify-event-schemas-supplier-api/src"; +import createAllocator from "../allocator"; +import { Deps } from "../deps"; + +function createSNSEvent(records: SNSEventRecord[]): SNSEvent { + return { + Records: records, + }; +} + +function createSNSEventRecord(message: string): SNSEventRecord { + return { + Sns: { + Message: message, + } as SNSEventRecord["Sns"], + } as SNSEventRecord; +} + +function createLetterEvent(domainId: string): LetterEvent { + const now = new Date().toISOString(); + + return $LetterEvent.parse({ + data: { + domainId, + groupId: "client_template", + origin: { + domain: "letter-rendering", + event: "f47ac10b-58cc-4372-a567-0e02b2c3d479", + source: "/data-plane/letter-rendering/prod/render-pdf", + subject: + "client/00f3b388-bbe9-41c9-9e76-052d37ee8988/letter-request/test", + }, + + specificationId: "spec-001", + billingRef: "billing-001", + status: "PENDING", + supplierId: "supplier-001", + }, + datacontenttype: "application/json", + dataschema: + "https://notify.nhs.uk/cloudevents/schemas/supplier-api/letter.PENDING.1.0.0.schema.json", + dataschemaversion: "1.0.0", + id: "f47ac10b-58cc-4372-a567-0e02b2c3d479", + plane: "data", + recordedtime: now, + severitynumber: 2, + severitytext: "INFO", + source: "/data-plane/supplier-api/prod/update-status", + specversion: "1.0", + subject: + "letter-origin/letter-rendering/letter/f47ac10b-58cc-4372-a567-0e02b2c3d479", + time: now, + traceparent: "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01", + type: "uk.nhs.notify.supplier-api.letter.PENDING.v1", + }); +} + +describe("allocator", () => { + const mockQueueUrl = + "https://sqs.eu-west-2.amazonaws.com/123456789012/test-queue.fifo"; + + let mockDeps: Deps; + + beforeEach(() => { + mockDeps = { + sqsClient: { send: jest.fn() } as unknown as SQSClient, + queueUrl: mockQueueUrl, + logger: { info: jest.fn(), error: jest.fn() } as unknown as pino.Logger, + }; + + jest.clearAllMocks(); + }); + + describe("createAllocator", () => { + it("should process a single SNS record and send message to SQS", async () => { + const letterEvent = createLetterEvent("id1"); + const snsEvent = createSNSEvent([ + createSNSEventRecord(JSON.stringify(letterEvent)), + ]); + + const handler = createAllocator(mockDeps); + await handler(snsEvent, mockDeep(), jest.fn()); + + expect(mockDeps.sqsClient.send).toHaveBeenCalledWith( + expect.objectContaining({ + input: { + QueueUrl: mockQueueUrl, + MessageBody: JSON.stringify(letterEvent), + MessageGroupId: "id1", + }, + }), + ); + }); + + it("should process multiple SNS records and send messages to SQS", async () => { + const letterEvent1 = createLetterEvent("id1"); + const letterEvent2 = createLetterEvent("id2"); + const letterEvent3 = createLetterEvent("id3"); + + const snsEvent = createSNSEvent([ + createSNSEventRecord(JSON.stringify(letterEvent1)), + createSNSEventRecord(JSON.stringify(letterEvent2)), + createSNSEventRecord(JSON.stringify(letterEvent3)), + ]); + + const handler = createAllocator(mockDeps); + await handler(snsEvent, mockDeep(), jest.fn()); + + expect(mockDeps.sqsClient.send).toHaveBeenCalledTimes(3); + + expect(mockDeps.sqsClient.send).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + input: { + QueueUrl: mockQueueUrl, + MessageBody: JSON.stringify(letterEvent1), + MessageGroupId: "id1", + }, + }), + ); + expect(mockDeps.sqsClient.send).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + input: { + QueueUrl: mockQueueUrl, + MessageBody: JSON.stringify(letterEvent2), + MessageGroupId: "id2", + }, + }), + ); + expect(mockDeps.sqsClient.send).toHaveBeenNthCalledWith( + 3, + expect.objectContaining({ + input: { + QueueUrl: mockQueueUrl, + MessageBody: JSON.stringify(letterEvent3), + MessageGroupId: "id3", + }, + }), + ); + }); + + it("should handle empty SNS event with no records", async () => { + const snsEvent = createSNSEvent([]); + + const handler = createAllocator(mockDeps); + await handler(snsEvent, mockDeep(), jest.fn()); + + expect(mockDeps.sqsClient.send).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/lambdas/allocation/src/allocator.ts b/lambdas/allocation/src/allocator.ts new file mode 100644 index 00000000..138fe1a6 --- /dev/null +++ b/lambdas/allocation/src/allocator.ts @@ -0,0 +1,37 @@ +import { SNSEvent, SNSEventRecord, SNSHandler } from "aws-lambda"; +import { SendMessageCommand } from "@aws-sdk/client-sqs"; +import { + $LetterEvent, + LetterEvent, +} from "@nhsdigital/nhs-notify-event-schemas-supplier-api/src"; +import { Deps } from "./deps"; + +export default function createAllocator(deps: Deps): SNSHandler { + return async (event: SNSEvent): Promise => { + // Allocation will be done under a future ticket. For now, just place events on the queue, + // adding a message group ID to permit the use of a FIFO queue + const sqsCommands: SendMessageCommand[] = event.Records.map((record) => + extractLetterEvent(record), + ).map((letterEvent) => buildSendMessageCommand(letterEvent, deps.queueUrl)); + + for (const sqsCommand of sqsCommands) { + deps.logger.info({ + description: "Placing message on queue", + MessageGroupId: sqsCommand.input.MessageGroupId, + }); + await deps.sqsClient.send(sqsCommand); + } + }; +} + +function extractLetterEvent(record: SNSEventRecord): LetterEvent { + return $LetterEvent.parse(JSON.parse(record.Sns.Message)); +} + +function buildSendMessageCommand(letterEvent: LetterEvent, queueUrl: string) { + return new SendMessageCommand({ + QueueUrl: queueUrl, + MessageBody: JSON.stringify(letterEvent), + MessageGroupId: letterEvent.data.domainId, + }); +} diff --git a/lambdas/allocation/src/deps.ts b/lambdas/allocation/src/deps.ts new file mode 100644 index 00000000..be2969a8 --- /dev/null +++ b/lambdas/allocation/src/deps.ts @@ -0,0 +1,19 @@ +import pino from "pino"; +import { SQSClient } from "@aws-sdk/client-sqs"; +import { envVars } from "./env"; + +export type Deps = { + sqsClient: SQSClient; + queueUrl: string; + logger: pino.Logger; +}; + +export function createDependenciesContainer(): Deps { + const log = pino(); + + return { + sqsClient: new SQSClient(), + queueUrl: envVars.QUEUE_URL, + logger: log, + }; +} diff --git a/lambdas/allocation/src/env.ts b/lambdas/allocation/src/env.ts new file mode 100644 index 00000000..ad8e5901 --- /dev/null +++ b/lambdas/allocation/src/env.ts @@ -0,0 +1,9 @@ +import { z } from "zod"; + +const EnvVarsSchema = z.object({ + QUEUE_URL: z.coerce.string(), +}); + +export type EnvVars = z.infer; + +export const envVars = EnvVarsSchema.parse(process.env); diff --git a/lambdas/allocation/src/index.ts b/lambdas/allocation/src/index.ts new file mode 100644 index 00000000..bb1f82ee --- /dev/null +++ b/lambdas/allocation/src/index.ts @@ -0,0 +1,7 @@ +import createAllocator from "./allocator"; +import { createDependenciesContainer } from "./deps"; + +const container = createDependenciesContainer(); + +// eslint-disable-next-line import-x/prefer-default-export +export const handler = createAllocator(container); diff --git a/lambdas/allocation/tsconfig.json b/lambdas/allocation/tsconfig.json new file mode 100644 index 00000000..24902365 --- /dev/null +++ b/lambdas/allocation/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": {}, + "extends": "../../tsconfig.base.json", + "include": [ + "src/**/*", + "jest.config.ts" + ] +} diff --git a/lambdas/api-handler/src/services/__tests__/letter-operations.test.ts b/lambdas/api-handler/src/services/__tests__/letter-operations.test.ts index 550fbeca..6aca8047 100644 --- a/lambdas/api-handler/src/services/__tests__/letter-operations.test.ts +++ b/lambdas/api-handler/src/services/__tests__/letter-operations.test.ts @@ -247,6 +247,7 @@ describe("enqueueLetterUpdateRequests function", () => { reasonCode: lettersToUpdate[0].reasonCode, reasonText: lettersToUpdate[0].reasonText, }), + MessageGroupId: lettersToUpdate[0].id, }, }), ); @@ -267,6 +268,7 @@ describe("enqueueLetterUpdateRequests function", () => { status: lettersToUpdate[1].status, supplierId: lettersToUpdate[1].supplierId, }), + MessageGroupId: lettersToUpdate[1].id, }, }), ); @@ -310,6 +312,7 @@ describe("enqueueLetterUpdateRequests function", () => { status: lettersToUpdate[1].status, supplierId: lettersToUpdate[1].supplierId, }), + MessageGroupId: lettersToUpdate[1].id, }, }), ); diff --git a/lambdas/api-handler/src/services/letter-operations.ts b/lambdas/api-handler/src/services/letter-operations.ts index c94e76b5..2738d2ec 100644 --- a/lambdas/api-handler/src/services/letter-operations.ts +++ b/lambdas/api-handler/src/services/letter-operations.ts @@ -94,6 +94,7 @@ export async function enqueueLetterUpdateRequests( CorrelationId: { DataType: "String", StringValue: correlationId }, }, MessageBody: JSON.stringify(request), + MessageGroupId: request.id, }); await deps.sqsClient.send(command); } catch (error) { diff --git a/lambdas/letter-updates-transformer/src/mappers/__tests__/letter-mapper.test.ts b/lambdas/letter-updates-transformer/src/mappers/__tests__/letter-mapper.test.ts index 93429c90..004ef701 100644 --- a/lambdas/letter-updates-transformer/src/mappers/__tests__/letter-mapper.test.ts +++ b/lambdas/letter-updates-transformer/src/mappers/__tests__/letter-mapper.test.ts @@ -22,7 +22,7 @@ describe("letter-mapper", () => { expect(event.dataschema).toBe( `https://notify.nhs.uk/cloudevents/schemas/supplier-api/letter.PRINTED.${event.dataschemaversion}.schema.json` ); - expect(event.dataschemaversion).toBe("1.0.6"); + expect(event.dataschemaversion).toMatch(/1\.\d+\.\d+/); expect(event.subject).toBe("letter-origin/supplier-api/letter/id1"); expect(event.time).toBe("2025-11-24T15:55:18.000Z"); expect(event.recordedtime).toBe("2025-11-24T15:55:18.000Z"); diff --git a/lambdas/supplier-events-forwarder/.eslintignore b/lambdas/supplier-events-forwarder/.eslintignore new file mode 100644 index 00000000..1521c8b7 --- /dev/null +++ b/lambdas/supplier-events-forwarder/.eslintignore @@ -0,0 +1 @@ +dist diff --git a/lambdas/supplier-events-forwarder/.gitignore b/lambdas/supplier-events-forwarder/.gitignore new file mode 100644 index 00000000..9b19292a --- /dev/null +++ b/lambdas/supplier-events-forwarder/.gitignore @@ -0,0 +1,4 @@ +.build +coverage +node_modules +dist diff --git a/lambdas/supplier-events-forwarder/jest.config.ts b/lambdas/supplier-events-forwarder/jest.config.ts new file mode 100644 index 00000000..f8e09e55 --- /dev/null +++ b/lambdas/supplier-events-forwarder/jest.config.ts @@ -0,0 +1,59 @@ +import type { Config } from "jest"; + +export const baseJestConfig: Config = { + preset: "ts-jest", + + // Automatically clear mock calls, instances, contexts and results before every test + clearMocks: true, + + // Indicates whether the coverage information should be collected while executing the test + collectCoverage: true, + + // The directory where Jest should output its coverage files + coverageDirectory: "./.reports/unit/coverage", + + // Indicates which provider should be used to instrument code for coverage + coverageProvider: "babel", + + coverageThreshold: { + global: { + branches: 100, + functions: 100, + lines: 100, + statements: -10, + }, + }, + + coveragePathIgnorePatterns: ["/__tests__/"], + transform: { "^.+\\.ts$": "ts-jest" }, + testPathIgnorePatterns: [".build"], + testMatch: ["**/?(*.)+(spec|test).[jt]s?(x)"], + + // Use this configuration option to add custom reporters to Jest + reporters: [ + "default", + [ + "jest-html-reporter", + { + pageTitle: "Test Report", + outputPath: "./.reports/unit/test-report.html", + includeFailureMsg: true, + }, + ], + ], + + // The test environment that will be used for testing + testEnvironment: "jsdom", +}; + +const utilsJestConfig = { + ...baseJestConfig, + + testEnvironment: "node", + + coveragePathIgnorePatterns: [ + ...(baseJestConfig.coveragePathIgnorePatterns ?? []), + ], +}; + +export default utilsJestConfig; diff --git a/lambdas/supplier-events-forwarder/package.json b/lambdas/supplier-events-forwarder/package.json new file mode 100644 index 00000000..0ce56473 --- /dev/null +++ b/lambdas/supplier-events-forwarder/package.json @@ -0,0 +1,26 @@ +{ + "dependencies": { + "@aws-sdk/client-firehose": "^3.925.0", + "aws-lambda": "^1.0.7", + "esbuild": "^0.25.11", + "pino": "^9.7.0", + "zod": "^4.1.11" + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.2", + "@types/jest": "^30.0.0", + "jest": "^30.2.0", + "jest-mock-extended": "^4.0.0", + "typescript": "^5.9.3" + }, + "name": "nhs-notify-supplier-events-forwarder", + "private": true, + "scripts": { + "lambda-build": "rm -rf dist && npx esbuild --bundle --minify --sourcemap --target=es2020 --platform=node --loader:.node=file --entry-names=[name] --outdir=dist src/index.ts", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "test:unit": "jest", + "typecheck": "tsc --noEmit" + }, + "version": "0.0.1" +} diff --git a/lambdas/supplier-events-forwarder/src/__tests__/forwarder.test.ts b/lambdas/supplier-events-forwarder/src/__tests__/forwarder.test.ts new file mode 100644 index 00000000..76aef5a8 --- /dev/null +++ b/lambdas/supplier-events-forwarder/src/__tests__/forwarder.test.ts @@ -0,0 +1,224 @@ +import { Context, SQSEvent, SQSRecord } from "aws-lambda"; +import { FirehoseClient, PutRecordCommand } from "@aws-sdk/client-firehose"; +import { mockDeep } from "jest-mock-extended"; +import pino from "pino"; +import createForwarder from "../forwarder"; +import { Deps } from "../deps"; + +function createSQSEvent(records: SQSRecord[]): SQSEvent { + return { + Records: records, + }; +} + +/** + * Creates an SQS record with a body containing the SNS notification wrapper. + * This simulates what SNS delivers to SQS when raw_message_delivery is false. + */ +function createSQSRecord( + body: string, + messageId = "test-sqs-msg-id", +): SQSRecord { + return { + messageId, + receiptHandle: "test-receipt-handle", + body, + attributes: { + ApproximateReceiveCount: "1", + SentTimestamp: "1704801600000", + SenderId: "123456789012", + ApproximateFirstReceiveTimestamp: "1704801600000", + }, + messageAttributes: {}, + md5OfBody: "test-md5", + eventSource: "aws:sqs", + eventSourceARN: "arn:aws:sqs:eu-west-2:123456789012:test-queue.fifo", + awsRegion: "eu-west-2", + }; +} + +/** + * Creates an SNS notification wrapper as it would appear in the SQS message body + * when raw_message_delivery is false. + */ +function createSnsNotificationWrapper( + message: string, + overrides: Partial<{ + MessageId: string; + TopicArn: string; + Subject: string; + }> = {}, +): string { + return JSON.stringify({ + Type: "Notification", + MessageId: overrides.MessageId ?? "test-sns-message-id", + TopicArn: + overrides.TopicArn ?? + "arn:aws:sns:eu-west-2:123456789012:test-topic.fifo", + Subject: overrides.Subject ?? "Test Subject", + Message: message, + Timestamp: "2026-01-09T12:00:00.000Z", + SignatureVersion: "1", + Signature: "test-signature", + SigningCertUrl: "https://sns.eu-west-2.amazonaws.com/cert.pem", + UnsubscribeUrl: "https://sns.eu-west-2.amazonaws.com/unsubscribe", + MessageAttributes: {}, + }); +} + +describe("forwarder", () => { + const mockDeliveryStreamName = "test-delivery-stream"; + + let mockFirehoseClient: jest.Mocked; + let mockDeps: Deps; + + beforeEach(() => { + mockFirehoseClient = { + send: jest.fn().mockResolvedValue({}), + } as unknown as jest.Mocked; + + mockDeps = { + firehoseClient: mockFirehoseClient, + deliveryStreamName: mockDeliveryStreamName, + logger: pino({ level: "silent" }), + }; + + jest.clearAllMocks(); + }); + + describe("createForwarder", () => { + it("should process a single SQS record and send to Firehose", async () => { + const message = JSON.stringify({ eventType: "test", data: "value" }); + const snsWrapper = createSnsNotificationWrapper(message); + const sqsRecord = createSQSRecord(snsWrapper); + const sqsEvent = createSQSEvent([sqsRecord]); + + const handler = createForwarder(mockDeps); + await handler(sqsEvent, mockDeep(), jest.fn()); + + expect(mockFirehoseClient.send).toHaveBeenCalledTimes(1); + expect(mockFirehoseClient.send).toHaveBeenCalledWith( + expect.any(PutRecordCommand), + ); + + const sentCommand = mockFirehoseClient.send.mock + .calls[0][0] as PutRecordCommand; + expect(sentCommand.input).toEqual({ + DeliveryStreamName: mockDeliveryStreamName, + Record: { + Data: Buffer.from(`${snsWrapper}\n`), + }, + }); + }); + + it("should process multiple SQS records and send to Firehose", async () => { + const message1 = JSON.stringify({ eventType: "test1" }); + const message2 = JSON.stringify({ eventType: "test2" }); + const message3 = JSON.stringify({ eventType: "test3" }); + + const snsWrapper1 = createSnsNotificationWrapper(message1, { + MessageId: "msg-1", + }); + const snsWrapper2 = createSnsNotificationWrapper(message2, { + MessageId: "msg-2", + }); + const snsWrapper3 = createSnsNotificationWrapper(message3, { + MessageId: "msg-3", + }); + + const sqsEvent = createSQSEvent([ + createSQSRecord(snsWrapper1, "sqs-1"), + createSQSRecord(snsWrapper2, "sqs-2"), + createSQSRecord(snsWrapper3, "sqs-3"), + ]); + + const handler = createForwarder(mockDeps); + await handler(sqsEvent, mockDeep(), jest.fn()); + + expect(mockFirehoseClient.send).toHaveBeenCalledTimes(3); + + const sentCommands = mockFirehoseClient.send.mock.calls.map( + (call) => call[0] as PutRecordCommand, + ); + + expect(sentCommands[0].input).toEqual({ + DeliveryStreamName: mockDeliveryStreamName, + Record: { + Data: Buffer.from(`${snsWrapper1}\n`), + }, + }); + + expect(sentCommands[1].input).toEqual({ + DeliveryStreamName: mockDeliveryStreamName, + Record: { + Data: Buffer.from(`${snsWrapper2}\n`), + }, + }); + + expect(sentCommands[2].input).toEqual({ + DeliveryStreamName: mockDeliveryStreamName, + Record: { + Data: Buffer.from(`${snsWrapper3}\n`), + }, + }); + }); + + it("should handle empty SQS event with no records", async () => { + const sqsEvent = createSQSEvent([]); + + const handler = createForwarder(mockDeps); + await handler(sqsEvent, mockDeep(), jest.fn()); + + expect(mockFirehoseClient.send).not.toHaveBeenCalled(); + }); + + it("should forward the SNS notification wrapper from SQS body to Firehose", async () => { + const message = JSON.stringify({ key: "value" }); + const snsWrapper = createSnsNotificationWrapper(message, { + MessageId: "unique-msg-id", + TopicArn: "arn:aws:sns:eu-west-2:123456789012:my-topic.fifo", + Subject: "My Subject", + }); + const sqsRecord = createSQSRecord(snsWrapper); + const sqsEvent = createSQSEvent([sqsRecord]); + + const handler = createForwarder(mockDeps); + await handler(sqsEvent, mockDeep(), jest.fn()); + + const sentCommand = mockFirehoseClient.send.mock + .calls[0][0] as PutRecordCommand; + const recordData = sentCommand.input.Record?.Data as Buffer; + const parsedData = JSON.parse(recordData.toString().replace(/\n$/, "")); + + expect(parsedData).toEqual({ + Type: "Notification", + MessageId: "unique-msg-id", + TopicArn: "arn:aws:sns:eu-west-2:123456789012:my-topic.fifo", + Subject: "My Subject", + Message: message, + Timestamp: "2026-01-09T12:00:00.000Z", + SignatureVersion: "1", + Signature: "test-signature", + SigningCertUrl: "https://sns.eu-west-2.amazonaws.com/cert.pem", + UnsubscribeUrl: "https://sns.eu-west-2.amazonaws.com/unsubscribe", + MessageAttributes: {}, + }); + }); + + it("should append newline to message for JSON Lines format", async () => { + const message = JSON.stringify({ key: "value" }); + const snsWrapper = createSnsNotificationWrapper(message); + const sqsRecord = createSQSRecord(snsWrapper); + const sqsEvent = createSQSEvent([sqsRecord]); + + const handler = createForwarder(mockDeps); + await handler(sqsEvent, mockDeep(), jest.fn()); + + const sentCommand = mockFirehoseClient.send.mock + .calls[0][0] as PutRecordCommand; + const recordData = sentCommand.input.Record?.Data as Buffer; + + expect(recordData.toString().endsWith("\n")).toBe(true); + }); + }); +}); diff --git a/lambdas/supplier-events-forwarder/src/deps.ts b/lambdas/supplier-events-forwarder/src/deps.ts new file mode 100644 index 00000000..3194a216 --- /dev/null +++ b/lambdas/supplier-events-forwarder/src/deps.ts @@ -0,0 +1,19 @@ +import pino from "pino"; +import { FirehoseClient } from "@aws-sdk/client-firehose"; +import { envVars } from "./env"; + +export type Deps = { + firehoseClient: FirehoseClient; + deliveryStreamName: string; + logger: pino.Logger; +}; + +export function createDependenciesContainer(): Deps { + const log = pino(); + + return { + firehoseClient: new FirehoseClient(), + deliveryStreamName: envVars.FIREHOSE_DELIVERY_STREAM_NAME, + logger: log, + }; +} diff --git a/lambdas/supplier-events-forwarder/src/env.ts b/lambdas/supplier-events-forwarder/src/env.ts new file mode 100644 index 00000000..f96c0446 --- /dev/null +++ b/lambdas/supplier-events-forwarder/src/env.ts @@ -0,0 +1,9 @@ +import { z } from "zod"; + +const EnvVarsSchema = z.object({ + FIREHOSE_DELIVERY_STREAM_NAME: z.coerce.string(), +}); + +export type EnvVars = z.infer; + +export const envVars = EnvVarsSchema.parse(process.env); diff --git a/lambdas/supplier-events-forwarder/src/forwarder.ts b/lambdas/supplier-events-forwarder/src/forwarder.ts new file mode 100644 index 00000000..b1686caa --- /dev/null +++ b/lambdas/supplier-events-forwarder/src/forwarder.ts @@ -0,0 +1,36 @@ +import { SQSEvent, SQSHandler, SQSRecord } from "aws-lambda"; +import { PutRecordCommand } from "@aws-sdk/client-firehose"; +import { Deps } from "./deps"; + +export default function createForwarder(deps: Deps): SQSHandler { + return async (event: SQSEvent): Promise => { + const firehoseCommands: PutRecordCommand[] = event.Records.map((record) => + buildPutRecordCommand(record, deps.deliveryStreamName), + ); + + for (const firehoseCommand of firehoseCommands) { + await deps.firehoseClient.send(firehoseCommand); + } + }; +} + +/** + * Builds a PutRecordCommand for Firehose. + * The SQS message body already contains the SNS notification wrapper + * (since raw_message_delivery is false on the SNS->SQS subscription), + * so we forward it directly to Firehose. + */ +function buildPutRecordCommand( + record: SQSRecord, + deliveryStreamName: string, +): PutRecordCommand { + // Add a newline to each record for proper JSON Lines format in S3 + const data = `${record.body}\n`; + + return new PutRecordCommand({ + DeliveryStreamName: deliveryStreamName, + Record: { + Data: Buffer.from(data), + }, + }); +} diff --git a/lambdas/supplier-events-forwarder/src/index.ts b/lambdas/supplier-events-forwarder/src/index.ts new file mode 100644 index 00000000..b3fbaa59 --- /dev/null +++ b/lambdas/supplier-events-forwarder/src/index.ts @@ -0,0 +1,7 @@ +import createForwarder from "./forwarder"; +import { createDependenciesContainer } from "./deps"; + +const container = createDependenciesContainer(); + +// eslint-disable-next-line import-x/prefer-default-export +export const handler = createForwarder(container); diff --git a/lambdas/supplier-events-forwarder/tsconfig.json b/lambdas/supplier-events-forwarder/tsconfig.json new file mode 100644 index 00000000..24902365 --- /dev/null +++ b/lambdas/supplier-events-forwarder/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": {}, + "extends": "../../tsconfig.base.json", + "include": [ + "src/**/*", + "jest.config.ts" + ] +} diff --git a/lambdas/upsert-letter/package.json b/lambdas/upsert-letter/package.json index 9444e714..0b014d2a 100644 --- a/lambdas/upsert-letter/package.json +++ b/lambdas/upsert-letter/package.json @@ -1,10 +1,10 @@ { "dependencies": { + "@types/aws-lambda": "^8.10.148", "esbuild": "^0.24.0" }, "devDependencies": { "@tsconfig/node22": "^22.0.2", - "@types/aws-lambda": "^8.10.148", "@types/jest": "^30.0.0", "jest": "^30.2.0", "jest-mock-extended": "^4.0.0", diff --git a/lambdas/upsert-letter/src/__tests__/index.test.ts b/lambdas/upsert-letter/src/__tests__/index.test.ts index d29b864b..5d2c910e 100644 --- a/lambdas/upsert-letter/src/__tests__/index.test.ts +++ b/lambdas/upsert-letter/src/__tests__/index.test.ts @@ -1,17 +1,12 @@ -import type { Context } from "aws-lambda"; +import type { Context, SQSEvent } from "aws-lambda"; import { mockDeep } from "jest-mock-extended"; import handler from ".."; describe("event-logging Lambda", () => { - it("logs the input event and returns 200", async () => { - const event = { foo: "bar" }; + it("completes successfully", async () => { + const event = { Records: [{ body: "{}" }] } as SQSEvent; const context = mockDeep(); const callback = jest.fn(); - const result = await handler(event, context, callback); - - expect(result).toEqual({ - statusCode: 200, - body: "Event logged", - }); + await handler(event, context, callback); }); }); diff --git a/lambdas/upsert-letter/src/index.ts b/lambdas/upsert-letter/src/index.ts index a165560c..a50f8323 100644 --- a/lambdas/upsert-letter/src/index.ts +++ b/lambdas/upsert-letter/src/index.ts @@ -1,12 +1,13 @@ // Replace me with the actual code for your Lambda function -import { Handler } from "aws-lambda"; +import { SQSEvent, SQSHandler } from "aws-lambda"; +import pino from "pino"; -const handler: Handler = async (event) => { - console.log("Received event:", event); - return { - statusCode: 200, - body: "Event logged", - }; +const log = pino(); + +export const handler: SQSHandler = async (event: SQSEvent) => { + for (const record of event.Records) { + log.info({ description: "Received event", message: record.body }); + } }; export default handler; diff --git a/package-lock.json b/package-lock.json index 7a29e4d7..ee166458 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@aws-sdk/client-sns": "^3.936.0", "@playwright/test": "^1.57.0", "ajv": "^8.17.1", + "aws-lambda": "^1.0.7", "get-east-asian-width": "^1.4.0", "js-yaml": "^4.1.0", "openapi-response-validator": "^12.1.3", @@ -125,7 +126,7 @@ }, "internal/events": { "name": "@nhsdigital/nhs-notify-event-schemas-supplier-api", - "version": "1.0.6", + "version": "1.0.7", "license": "MIT", "dependencies": { "@asyncapi/bundler": "^0.6.4", @@ -167,6 +168,47 @@ "typescript": "^5.9.3" } }, + "lambdas/allocation": { + "name": "nhs-notify-supplier-allocation", + "version": "0.0.1", + "dependencies": { + "@aws-sdk/client-sqs": "^3.925.0", + "@nhsdigital/nhs-notify-event-schemas-supplier-api": "*", + "aws-lambda": "^1.0.7", + "esbuild": "^0.25.11", + "pino": "^9.7.0", + "zod": "^4.1.11" + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.2", + "@types/jest": "^30.0.0", + "jest": "^30.2.0", + "jest-mock-extended": "^4.0.0", + "typescript": "^5.9.3" + } + }, + "lambdas/allocation/node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, "lambdas/api-handler": { "name": "nhs-notify-supplier-api-handler", "version": "0.0.1", @@ -1177,6 +1219,46 @@ "@esbuild/win32-x64": "0.24.2" } }, + "lambdas/supplier-events-forwarder": { + "name": "nhs-notify-supplier-events-forwarder", + "version": "0.0.1", + "dependencies": { + "@aws-sdk/client-firehose": "^3.925.0", + "aws-lambda": "^1.0.7", + "esbuild": "^0.25.11", + "pino": "^9.7.0", + "zod": "^4.1.11" + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.2", + "@types/jest": "^30.0.0", + "jest": "^30.2.0", + "jest-mock-extended": "^4.0.0", + "typescript": "^5.9.3" + } + }, + "lambdas/supplier-events-forwarder/node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, "lambdas/upsert-letter": { "name": "nhs-notify-supplier-api-upsert-letter", "version": "0.0.1", @@ -1997,6 +2079,512 @@ "node": ">=18.0.0" } }, + "node_modules/@aws-sdk/client-firehose": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-firehose/-/client-firehose-3.965.0.tgz", + "integrity": "sha512-kMMta0tkyTfJ7H5+RmtRHrUZXjz1UqOqJwWegS6+R0bp3v47n/BRuSn3udTNIWHsE2N87cRre07XTeCQ3RFyPw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.965.0", + "@aws-sdk/credential-provider-node": "3.965.0", + "@aws-sdk/middleware-host-header": "3.965.0", + "@aws-sdk/middleware-logger": "3.965.0", + "@aws-sdk/middleware-recursion-detection": "3.965.0", + "@aws-sdk/middleware-user-agent": "3.965.0", + "@aws-sdk/region-config-resolver": "3.965.0", + "@aws-sdk/types": "3.965.0", + "@aws-sdk/util-endpoints": "3.965.0", + "@aws-sdk/util-user-agent-browser": "3.965.0", + "@aws-sdk/util-user-agent-node": "3.965.0", + "@smithy/config-resolver": "^4.4.5", + "@smithy/core": "^3.20.0", + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/hash-node": "^4.2.7", + "@smithy/invalid-dependency": "^4.2.7", + "@smithy/middleware-content-length": "^4.2.7", + "@smithy/middleware-endpoint": "^4.4.1", + "@smithy/middleware-retry": "^4.4.17", + "@smithy/middleware-serde": "^4.2.8", + "@smithy/middleware-stack": "^4.2.7", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.16", + "@smithy/util-defaults-mode-node": "^4.2.19", + "@smithy/util-endpoints": "^3.2.7", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-retry": "^4.2.7", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/client-sso": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.965.0.tgz", + "integrity": "sha512-iv2tr+n4aZ+nPUFFvG00hISPuEd4DU+1/Q8rPAYKXsM+vEPJ2nAnP5duUOa2fbOLIUCRxX3dcQaQaghVHDHzQw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.965.0", + "@aws-sdk/middleware-host-header": "3.965.0", + "@aws-sdk/middleware-logger": "3.965.0", + "@aws-sdk/middleware-recursion-detection": "3.965.0", + "@aws-sdk/middleware-user-agent": "3.965.0", + "@aws-sdk/region-config-resolver": "3.965.0", + "@aws-sdk/types": "3.965.0", + "@aws-sdk/util-endpoints": "3.965.0", + "@aws-sdk/util-user-agent-browser": "3.965.0", + "@aws-sdk/util-user-agent-node": "3.965.0", + "@smithy/config-resolver": "^4.4.5", + "@smithy/core": "^3.20.0", + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/hash-node": "^4.2.7", + "@smithy/invalid-dependency": "^4.2.7", + "@smithy/middleware-content-length": "^4.2.7", + "@smithy/middleware-endpoint": "^4.4.1", + "@smithy/middleware-retry": "^4.4.17", + "@smithy/middleware-serde": "^4.2.8", + "@smithy/middleware-stack": "^4.2.7", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.16", + "@smithy/util-defaults-mode-node": "^4.2.19", + "@smithy/util-endpoints": "^3.2.7", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-retry": "^4.2.7", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/core": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.965.0.tgz", + "integrity": "sha512-aq9BhQxdHit8UUJ9C0im9TtuKeK0pT6NXmNJxMTCFeStI7GG7ImIsSislg3BZTIifVg1P6VLdzMyz9de85iutQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.965.0", + "@aws-sdk/xml-builder": "3.965.0", + "@smithy/core": "^3.20.0", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/signature-v4": "^5.3.7", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.965.0.tgz", + "integrity": "sha512-mdGnaIjMxTIjsb70dEj3VsWPWpoq1V5MWzBSfJq2H8zgMBXjn6d5/qHP8HMf53l9PrsgqzMpXGv3Av549A2x1g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.965.0", + "@aws-sdk/types": "3.965.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.965.0.tgz", + "integrity": "sha512-YuGQel9EgA/z25oeLM+GYYQS750+8AESvr7ZEmVnRPL0sg+K3DmGqdv+9gFjFd0UkLjTlC/jtbP2cuY6UcPiHQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.965.0", + "@aws-sdk/types": "3.965.0", + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", + "@smithy/util-stream": "^4.5.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.965.0.tgz", + "integrity": "sha512-xRo72Prer5s0xYVSCxCymVIRSqrVlevK5cmU0GWq9yJtaBNpnx02jwdJg80t/Ni7pgbkQyFWRMcq38c1tc6M/w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.965.0", + "@aws-sdk/credential-provider-env": "3.965.0", + "@aws-sdk/credential-provider-http": "3.965.0", + "@aws-sdk/credential-provider-login": "3.965.0", + "@aws-sdk/credential-provider-process": "3.965.0", + "@aws-sdk/credential-provider-sso": "3.965.0", + "@aws-sdk/credential-provider-web-identity": "3.965.0", + "@aws-sdk/nested-clients": "3.965.0", + "@aws-sdk/types": "3.965.0", + "@smithy/credential-provider-imds": "^4.2.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/credential-provider-login": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.965.0.tgz", + "integrity": "sha512-43/H8Qku8LHyugbhLo8kjD+eauhybCeVkmrnvWl8bXNHJP7xi1jCdtBQJKKJqiIHZws4MOEwkji8kFdAVRCe6g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.965.0", + "@aws-sdk/nested-clients": "3.965.0", + "@aws-sdk/types": "3.965.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.965.0.tgz", + "integrity": "sha512-cRxmMHF+Zh2lkkkEVduKl+8OQdtg/DhYA69+/7SPSQURlgyjFQGlRQ58B7q8abuNlrGT3sV+UzeOylZpJbV61Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.965.0", + "@aws-sdk/credential-provider-http": "3.965.0", + "@aws-sdk/credential-provider-ini": "3.965.0", + "@aws-sdk/credential-provider-process": "3.965.0", + "@aws-sdk/credential-provider-sso": "3.965.0", + "@aws-sdk/credential-provider-web-identity": "3.965.0", + "@aws-sdk/types": "3.965.0", + "@smithy/credential-provider-imds": "^4.2.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.965.0.tgz", + "integrity": "sha512-gmkPmdiR0yxnTzLPDb7rwrDhGuCUjtgnj8qWP+m0gSz/W43rR4jRPVEf6DUX2iC+ImQhxo3NFhuB3V42Kzo3TQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.965.0", + "@aws-sdk/types": "3.965.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.965.0.tgz", + "integrity": "sha512-N01AYvtCqG3Wo/s/LvYt19ity18/FqggiXT+elAs3X9Om/Wfx+hw9G+i7jaDmy+/xewmv8AdQ2SK5Q30dXw/Fw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.965.0", + "@aws-sdk/core": "3.965.0", + "@aws-sdk/token-providers": "3.965.0", + "@aws-sdk/types": "3.965.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.965.0.tgz", + "integrity": "sha512-T4gMZ2JzXnfxe1oTD+EDGLSxFfk1+WkLZdiHXEMZp8bFI1swP/3YyDFXI+Ib9Uq1JhnAmrCXtOnkicKEhDkdhQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.965.0", + "@aws-sdk/nested-clients": "3.965.0", + "@aws-sdk/types": "3.965.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.965.0.tgz", + "integrity": "sha512-SfpSYqoPOAmdb3DBsnNsZ0vix+1VAtkUkzXM79JL3R5IfacpyKE2zytOgVAQx/FjhhlpSTwuXd+LRhUEVb3MaA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.965.0", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/middleware-logger": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.965.0.tgz", + "integrity": "sha512-gjUvJRZT1bUABKewnvkj51LAynFrfz2h5DYAg5/2F4Utx6UOGByTSr9Rq8JCLbURvvzAbCtcMkkIJRxw+8Zuzw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.965.0", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.965.0.tgz", + "integrity": "sha512-6dvD+18Ni14KCRu+tfEoNxq1sIGVp9tvoZDZ7aMvpnA7mDXuRLrOjRQ/TAZqXwr9ENKVGyxcPl0cRK8jk1YWjA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.965.0", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.965.0.tgz", + "integrity": "sha512-RBEYVGgu/WeAt+H/qLrGc+t8LqAUkbyvh3wBfTiuAD+uBcWsKnvnB1iSBX75FearC0fmoxzXRUc0PMxMdqpjJQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.965.0", + "@aws-sdk/types": "3.965.0", + "@aws-sdk/util-endpoints": "3.965.0", + "@smithy/core": "^3.20.0", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/nested-clients": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.965.0.tgz", + "integrity": "sha512-muNVUjUEU+/KLFrLzQ8PMXyw4+a/MP6t4GIvwLtyx/kH0rpSy5s0YmqacMXheuIe6F/5QT8uksXGNAQenitkGQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.965.0", + "@aws-sdk/middleware-host-header": "3.965.0", + "@aws-sdk/middleware-logger": "3.965.0", + "@aws-sdk/middleware-recursion-detection": "3.965.0", + "@aws-sdk/middleware-user-agent": "3.965.0", + "@aws-sdk/region-config-resolver": "3.965.0", + "@aws-sdk/types": "3.965.0", + "@aws-sdk/util-endpoints": "3.965.0", + "@aws-sdk/util-user-agent-browser": "3.965.0", + "@aws-sdk/util-user-agent-node": "3.965.0", + "@smithy/config-resolver": "^4.4.5", + "@smithy/core": "^3.20.0", + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/hash-node": "^4.2.7", + "@smithy/invalid-dependency": "^4.2.7", + "@smithy/middleware-content-length": "^4.2.7", + "@smithy/middleware-endpoint": "^4.4.1", + "@smithy/middleware-retry": "^4.4.17", + "@smithy/middleware-serde": "^4.2.8", + "@smithy/middleware-stack": "^4.2.7", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.16", + "@smithy/util-defaults-mode-node": "^4.2.19", + "@smithy/util-endpoints": "^3.2.7", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-retry": "^4.2.7", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.965.0.tgz", + "integrity": "sha512-RoMhu9ly2B0coxn8ctXosPP2WmDD0MkQlZGLjoYHQUOCBmty5qmCxOqBmBDa6wbWbB8xKtMQ/4VXloQOgzjHXg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.965.0", + "@smithy/config-resolver": "^4.4.5", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/token-providers": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.965.0.tgz", + "integrity": "sha512-aR0qxg0b8flkXJVE+CM1gzo7uJ57md50z2eyCwofC0QIz5Y0P7/7vvb9/dmUQt6eT9XRN5iRcUqq2IVxVDvJOw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.965.0", + "@aws-sdk/nested-clients": "3.965.0", + "@aws-sdk/types": "3.965.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/types": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.965.0.tgz", + "integrity": "sha512-jvodoJdMavvg8faN7co58vVJRO5MVep4JFPRzUNCzpJ98BDqWDk/ad045aMJcmxkLzYLS2UAnUmqjJ/tUPNlzQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/util-endpoints": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.965.0.tgz", + "integrity": "sha512-WqSCB0XIsGUwZWvrYkuoofi2vzoVHqyeJ2kN+WyoOsxPLTiQSBIoqm/01R/qJvoxwK/gOOF7su9i84Vw2NQQpQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.965.0", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@smithy/util-endpoints": "^3.2.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.965.0.tgz", + "integrity": "sha512-Xiza/zMntQGpkd2dETQeAK8So1pg5+STTzpcdGWxj5q0jGO5ayjqT/q1Q7BrsX5KIr6PvRkl9/V7lLCv04wGjQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.965.0", + "@smithy/types": "^4.11.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.965.0.tgz", + "integrity": "sha512-kokIHUfNT3/P55E4fUJJrFHuuA9BbjFKUIxoLrd3UaRfdafT0ScRfg2eaZie6arf60EuhlUIZH0yALxttMEjxQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.965.0", + "@aws-sdk/types": "3.965.0", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/types": "^4.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/xml-builder": { + "version": "3.965.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.965.0.tgz", + "integrity": "sha512-Tcod25/BTupraQwtb+Q+GX8bmEZfxIFjjJ/AvkhUZsZlkPeVluzq1uu3Oeqf145DCdMjzLIN6vab5MrykbDP+g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.11.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/client-kinesis": { "version": "3.964.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-kinesis/-/client-kinesis-3.964.0.tgz", @@ -6580,9 +7168,9 @@ } }, "node_modules/@smithy/core": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.20.0.tgz", - "integrity": "sha512-WsSHCPq/neD5G/MkK4csLI5Y5Pkd9c1NMfpYEKeghSGaD4Ja1qLIohRQf2D5c1Uy5aXp76DeKHkzWZ9KAlHroQ==", + "version": "3.20.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.20.1.tgz", + "integrity": "sha512-wOboSEdQ85dbKAJ0zL+wQ6b0HTSBRhtGa0PYKysQXkRg+vK0tdCRRVruiFM2QMprkOQwSYOnwF4og96PAaEGag==", "license": "Apache-2.0", "dependencies": { "@smithy/middleware-serde": "^4.2.8", @@ -6800,12 +7388,12 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.1.tgz", - "integrity": "sha512-gpLspUAoe6f1M6H0u4cVuFzxZBrsGZmjx2O9SigurTx4PbntYa4AJ+o0G0oGm1L2oSX6oBhcGHwrfJHup2JnJg==", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.2.tgz", + "integrity": "sha512-mqpAdux0BNmZu/SqkFhQEnod4fX23xxTvU2LUpmKp0JpSI+kPYCiHJMmzREr8yxbNxKL2/DU1UZm9i++ayU+2g==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.20.0", + "@smithy/core": "^3.20.1", "@smithy/middleware-serde": "^4.2.8", "@smithy/node-config-provider": "^4.3.7", "@smithy/shared-ini-file-loader": "^4.4.2", @@ -6819,15 +7407,15 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.17", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.17.tgz", - "integrity": "sha512-MqbXK6Y9uq17h+4r0ogu/sBT6V/rdV+5NvYL7ZV444BKfQygYe8wAhDrVXagVebN6w2RE0Fm245l69mOsPGZzg==", + "version": "4.4.18", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.18.tgz", + "integrity": "sha512-E5hulijA59nBk/zvcwVMaS7FG7Y4l6hWA9vrW018r+8kiZef4/ETQaPI4oY+3zsy9f6KqDv3c4VKtO4DwwgpCg==", "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^4.3.7", "@smithy/protocol-http": "^5.3.7", "@smithy/service-error-classification": "^4.2.7", - "@smithy/smithy-client": "^4.10.2", + "@smithy/smithy-client": "^4.10.3", "@smithy/types": "^4.11.0", "@smithy/util-middleware": "^4.2.7", "@smithy/util-retry": "^4.2.7", @@ -6994,13 +7582,13 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.10.2", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.10.2.tgz", - "integrity": "sha512-D5z79xQWpgrGpAHb054Fn2CCTQZpog7JELbVQ6XAvXs5MNKWf28U9gzSBlJkOyMl9LA1TZEjRtwvGXfP0Sl90g==", + "version": "4.10.3", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.10.3.tgz", + "integrity": "sha512-EfECiO/0fAfb590LBnUe7rI5ux7XfquQ8LBzTe7gxw0j9QW/q8UT/EHWHlxV/+jhQ3+Ssga9uUYXCQgImGMbNg==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.20.0", - "@smithy/middleware-endpoint": "^4.4.1", + "@smithy/core": "^3.20.1", + "@smithy/middleware-endpoint": "^4.4.2", "@smithy/middleware-stack": "^4.2.7", "@smithy/protocol-http": "^5.3.7", "@smithy/types": "^4.11.0", @@ -7101,13 +7689,13 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.16", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.16.tgz", - "integrity": "sha512-/eiSP3mzY3TsvUOYMeL4EqUX6fgUOj2eUOU4rMMgVbq67TiRLyxT7Xsjxq0bW3OwuzK009qOwF0L2OgJqperAQ==", + "version": "4.3.17", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.17.tgz", + "integrity": "sha512-dwN4GmivYF1QphnP3xJESXKtHvkkvKHSZI8GrSKMVoENVSKW2cFPRYC4ZgstYjUHdR3zwaDkIaTDIp26JuY7Cw==", "license": "Apache-2.0", "dependencies": { "@smithy/property-provider": "^4.2.7", - "@smithy/smithy-client": "^4.10.2", + "@smithy/smithy-client": "^4.10.3", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, @@ -7116,16 +7704,16 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.19", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.19.tgz", - "integrity": "sha512-3a4+4mhf6VycEJyHIQLypRbiwG6aJvbQAeRAVXydMmfweEPnLLabRbdyo/Pjw8Rew9vjsh5WCdhmDaHkQnhhhA==", + "version": "4.2.20", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.20.tgz", + "integrity": "sha512-VD/I4AEhF1lpB3B//pmOIMBNLMrtdMXwy9yCOfa2QkJGDr63vH3RqPbSAKzoGMov3iryCxTXCxSsyGmEB8PDpg==", "license": "Apache-2.0", "dependencies": { "@smithy/config-resolver": "^4.4.5", "@smithy/credential-provider-imds": "^4.2.7", "@smithy/node-config-provider": "^4.3.7", "@smithy/property-provider": "^4.2.7", - "@smithy/smithy-client": "^4.10.2", + "@smithy/smithy-client": "^4.10.3", "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, @@ -16953,6 +17541,10 @@ "resolved": "docs", "link": true }, + "node_modules/nhs-notify-supplier-allocation": { + "resolved": "lambdas/allocation", + "link": true + }, "node_modules/nhs-notify-supplier-api-handler": { "resolved": "lambdas/api-handler", "link": true @@ -16981,6 +17573,10 @@ "resolved": "lambdas/authorizer", "link": true }, + "node_modules/nhs-notify-supplier-events-forwarder": { + "resolved": "lambdas/supplier-events-forwarder", + "link": true + }, "node_modules/nhsuk-frontend": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/nhsuk-frontend/-/nhsuk-frontend-10.2.2.tgz", @@ -21776,13 +22372,6 @@ "node": ">=18.17" } }, - "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "extraneous": true, - "license": "MIT" - }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", diff --git a/package.json b/package.json index 10db0476..377e3f0d 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "@aws-sdk/client-sns": "^3.936.0", "@playwright/test": "^1.57.0", "ajv": "^8.17.1", + "aws-lambda": "^1.0.7", "js-yaml": "^4.1.0", "get-east-asian-width": "^1.4.0", "openapi-response-validator": "^12.1.3",