From d66ac1bd1bd8fdab6852385a8ab4928aae0de20a Mon Sep 17 00:00:00 2001 From: Steve Buxton Date: Wed, 7 Jan 2026 16:41:12 +0000 Subject: [PATCH 1/3] CCM-12999 Letter amendments queue --- .vscode/settings.json | 3 +- .../terraform/components/api/README.md | 7 +- ...ource_mapping_status_updates_to_handler.tf | 7 +- ...mbda_event_source_mapping_upsert_letter.tf | 3 +- .../api/module_lambda_allocation.tf | 72 ++ .../api/module_lambda_letter_status_update.tf | 2 +- .../api/module_lambda_patch_letter.tf | 4 +- .../api/module_lambda_post_letters.tf | 4 +- ...module_lambda_supplier_events_forwarder.tf | 86 ++ .../api/module_lambda_upsert_letter.tf | 2 +- .../api/module_sqs_amendments_queue.tf | 47 + ...tf => module_sqs_supplier_events_queue.tf} | 21 +- ... => module_sqs_supplier_requests_queue.tf} | 7 +- ...ns_topic_subscription_allocation_lambda.tf | 13 + ...sns_topic_subscription_amendments_queue.tf | 6 + ...ubscription_eventsub_sqs_letter_updates.tf | 5 - ...iption_supplier_events_forwarder_lambda.tf | 18 + .../terraform/modules/eventsub/README.md | 4 +- ...atch_metric_alarm_sns_delivery_failures.tf | 2 +- .../terraform/modules/eventsub/outputs.tf | 22 +- .../{sns_topic.tf => sns_topic_event_bus.tf} | 4 +- .../modules/eventsub/sns_topic_policy.tf | 80 +- .../sns_topic_subscription_firehose.tf | 4 +- .../modules/eventsub/sns_topic_supplier.tf | 27 + internal/datastore/src/types.md | 3 + lambdas/allocation/.eslintignore | 1 + lambdas/allocation/.gitignore | 4 + lambdas/allocation/jest.config.ts | 60 ++ lambdas/allocation/package.json | 27 + .../src/__tests__/allocator.test.ts | 159 ++++ lambdas/allocation/src/allocator.ts | 37 + lambdas/allocation/src/deps.ts | 19 + lambdas/allocation/src/env.ts | 9 + lambdas/allocation/src/index.ts | 7 + lambdas/allocation/tsconfig.json | 8 + .../src/services/letter-operations.ts | 1 + .../supplier-events-forwarder/.eslintignore | 1 + lambdas/supplier-events-forwarder/.gitignore | 4 + .../supplier-events-forwarder/jest.config.ts | 59 ++ .../supplier-events-forwarder/package.json | 26 + .../src/__tests__/forwarder.test.ts | 224 +++++ lambdas/supplier-events-forwarder/src/deps.ts | 19 + lambdas/supplier-events-forwarder/src/env.ts | 9 + .../src/forwarder.ts | 37 + .../supplier-events-forwarder/src/index.ts | 7 + .../supplier-events-forwarder/tsconfig.json | 8 + package-lock.json | 881 +++++++++++++++--- 47 files changed, 1899 insertions(+), 161 deletions(-) create mode 100644 infrastructure/terraform/components/api/module_lambda_allocation.tf create mode 100644 infrastructure/terraform/components/api/module_lambda_supplier_events_forwarder.tf create mode 100644 infrastructure/terraform/components/api/module_sqs_amendments_queue.tf rename infrastructure/terraform/components/api/{module_sqs_letter_updates.tf => module_sqs_supplier_events_queue.tf} (67%) rename infrastructure/terraform/components/api/{module_sqs_letter_status_updates.tf => module_sqs_supplier_requests_queue.tf} (74%) create mode 100644 infrastructure/terraform/components/api/sns_topic_subscription_allocation_lambda.tf create mode 100644 infrastructure/terraform/components/api/sns_topic_subscription_amendments_queue.tf delete mode 100644 infrastructure/terraform/components/api/sns_topic_subscription_eventsub_sqs_letter_updates.tf create mode 100644 infrastructure/terraform/components/api/sns_topic_subscription_supplier_events_forwarder_lambda.tf rename infrastructure/terraform/modules/eventsub/{sns_topic.tf => sns_topic_event_bus.tf} (95%) create mode 100644 infrastructure/terraform/modules/eventsub/sns_topic_supplier.tf create mode 100644 lambdas/allocation/.eslintignore create mode 100644 lambdas/allocation/.gitignore create mode 100644 lambdas/allocation/jest.config.ts create mode 100644 lambdas/allocation/package.json create mode 100644 lambdas/allocation/src/__tests__/allocator.test.ts create mode 100644 lambdas/allocation/src/allocator.ts create mode 100644 lambdas/allocation/src/deps.ts create mode 100644 lambdas/allocation/src/env.ts create mode 100644 lambdas/allocation/src/index.ts create mode 100644 lambdas/allocation/tsconfig.json create mode 100644 lambdas/supplier-events-forwarder/.eslintignore create mode 100644 lambdas/supplier-events-forwarder/.gitignore create mode 100644 lambdas/supplier-events-forwarder/jest.config.ts create mode 100644 lambdas/supplier-events-forwarder/package.json create mode 100644 lambdas/supplier-events-forwarder/src/__tests__/forwarder.test.ts create mode 100644 lambdas/supplier-events-forwarder/src/deps.ts create mode 100644 lambdas/supplier-events-forwarder/src/env.ts create mode 100644 lambdas/supplier-events-forwarder/src/forwarder.ts create mode 100644 lambdas/supplier-events-forwarder/src/index.ts create mode 100644 lambdas/supplier-events-forwarder/tsconfig.json diff --git a/.vscode/settings.json b/.vscode/settings.json index d7c02400..3f98619f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,5 +10,6 @@ "**/Thumbs.db": true, ".github": false, ".vscode": false - } + }, + "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/infrastructure/terraform/components/api/README.md b/infrastructure/terraform/components/api/README.md index 4661f17f..e9e4b803 100644 --- a/infrastructure/terraform/components/api/README.md +++ b/infrastructure/terraform/components/api/README.md @@ -39,6 +39,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 | @@ -49,7 +51,6 @@ 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 | | [mi\_updates\_transformer](#module\_mi\_updates\_transformer) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.26/terraform-lambda.zip | n/a | @@ -57,7 +58,9 @@ No requirements. | [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 79b3b3f0..724e529a 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 201e1018..9678ae16 100644 --- a/infrastructure/terraform/components/api/module_lambda_upsert_letter.tf +++ b/infrastructure/terraform/components/api/module_lambda_upsert_letter.tf @@ -82,7 +82,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..40c8a7f2 --- /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 = "amendments" + + 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 = "AllowSNSToSendAmendments" + 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}-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_supplier_events_queue.tf similarity index 67% rename from infrastructure/terraform/components/api/module_sqs_letter_updates.tf rename to infrastructure/terraform/components/api/module_sqs_supplier_events_queue.tf index 472afb81..01baaa1f 100644 --- a/infrastructure/terraform/components/api/module_sqs_letter_updates.tf +++ b/infrastructure/terraform/components/api/module_sqs_supplier_events_queue.tf @@ -1,4 +1,4 @@ -module "sqs_letter_updates" { +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 @@ -6,20 +6,23 @@ module "sqs_letter_updates" { environment = var.environment project = var.project region = var.region - name = "letter-updates" + name = "supplier-events" + + 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.letter_updates_queue_policy.json + sqs_policy_overload = data.aws_iam_policy_document.supplier_events_queue_policy.json } -data "aws_iam_policy_document" "letter_updates_queue_policy" { +data "aws_iam_policy_document" "supplier_events_queue_policy" { version = "2012-10-17" statement { - sid = "AllowSNSToSendMessage" + sid = "AllowSNSToSendSupplierEvents" effect = "Allow" principals { @@ -32,13 +35,13 @@ data "aws_iam_policy_document" "letter_updates_queue_policy" { ] resources = [ - "arn:aws:sqs:${var.region}:${var.aws_account_id}:${var.project}-${var.environment}-${var.component}-letter-updates-queue" + "arn:aws:sqs:${var.region}:${var.aws_account_id}:${var.project}-${var.environment}-${var.component}-supplier-events-queue.fifo" ] condition { test = "ArnEquals" variable = "aws:SourceArn" - values = [module.eventsub.sns_topic.arn] + values = [module.eventsub.sns_topic_supplier.arn] } } @@ -59,13 +62,13 @@ data "aws_iam_policy_document" "letter_updates_queue_policy" { ] resources = [ - "arn:aws:sqs:${var.region}:${var.aws_account_id}:${var.project}-${var.environment}-${var.component}-letter-updates-queue" + "arn:aws:sqs:${var.region}:${var.aws_account_id}:${var.project}-${var.environment}-${var.component}-amendments-queue.fifo" ] condition { test = "ArnEquals" variable = "aws:SourceArn" - values = [module.eventsub.sns_topic.arn] + 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 74% 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..779fcde5 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 = "supplier-requests" 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..217e08ca 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 = "AllowAllSNSActionsFromAccountSupplier" 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 = "AllowAllSNSActionsFromSharedAccountSupplier" 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/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..c57d08ba --- /dev/null +++ b/lambdas/allocation/src/__tests__/allocator.test.ts @@ -0,0 +1,159 @@ +import { Context, SNSEvent, SNSEventRecord } from "aws-lambda"; +import { SQSClient } 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/letter-operations.ts b/lambdas/api-handler/src/services/letter-operations.ts index 558c2c90..62839f1c 100644 --- a/lambdas/api-handler/src/services/letter-operations.ts +++ b/lambdas/api-handler/src/services/letter-operations.ts @@ -111,6 +111,7 @@ export async function enqueueLetterUpdateRequests( const entries = batch.map((request, idx) => ({ Id: `${i + batchIdx}-${idx}`, // unique per batch entry MessageBody: JSON.stringify(request), + MessageGroupId: request.id, MessageAttributes: { CorrelationId: { DataType: "String", StringValue: correlationId }, }, 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..7e27e1cc --- /dev/null +++ b/lambdas/supplier-events-forwarder/src/forwarder.ts @@ -0,0 +1,37 @@ +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) { + deps.logger.info({ description: "Sending firehose command" }); + 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/package-lock.json b/package-lock.json index 61aace1f..ef793779 100644 --- a/package-lock.json +++ b/package-lock.json @@ -165,6 +165,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", @@ -2030,6 +2071,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", @@ -2450,6 +2531,512 @@ "node": ">=18.0.0" } }, + "node_modules/@aws-sdk/client-firehose": { + "version": "3.971.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-firehose/-/client-firehose-3.971.0.tgz", + "integrity": "sha512-j4QhvqniGcaZYpec7pnc/YPC4ZOVSo7+slW4USYr6fRtgzRtZ/B9kzXFxbsT2ulbT9X3BNfgzfXm8SicPWf85A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.970.0", + "@aws-sdk/credential-provider-node": "3.971.0", + "@aws-sdk/middleware-host-header": "3.969.0", + "@aws-sdk/middleware-logger": "3.969.0", + "@aws-sdk/middleware-recursion-detection": "3.969.0", + "@aws-sdk/middleware-user-agent": "3.970.0", + "@aws-sdk/region-config-resolver": "3.969.0", + "@aws-sdk/types": "3.969.0", + "@aws-sdk/util-endpoints": "3.970.0", + "@aws-sdk/util-user-agent-browser": "3.969.0", + "@aws-sdk/util-user-agent-node": "3.971.0", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.20.6", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.7", + "@smithy/middleware-retry": "^4.4.23", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.10.8", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@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.22", + "@smithy/util-defaults-mode-node": "^4.2.25", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/client-sso": { + "version": "3.971.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.971.0.tgz", + "integrity": "sha512-Xx+w6DQqJxDdymYyIxyKJnRzPvVJ4e/Aw0czO7aC9L/iraaV7AG8QtRe93OGW6aoHSh72CIiinnpJJfLsQqP4g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.970.0", + "@aws-sdk/middleware-host-header": "3.969.0", + "@aws-sdk/middleware-logger": "3.969.0", + "@aws-sdk/middleware-recursion-detection": "3.969.0", + "@aws-sdk/middleware-user-agent": "3.970.0", + "@aws-sdk/region-config-resolver": "3.969.0", + "@aws-sdk/types": "3.969.0", + "@aws-sdk/util-endpoints": "3.970.0", + "@aws-sdk/util-user-agent-browser": "3.969.0", + "@aws-sdk/util-user-agent-node": "3.971.0", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.20.6", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.7", + "@smithy/middleware-retry": "^4.4.23", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.10.8", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@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.22", + "@smithy/util-defaults-mode-node": "^4.2.25", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/core": { + "version": "3.970.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.970.0.tgz", + "integrity": "sha512-klpzObldOq8HXzDjDlY6K8rMhYZU6mXRz6P9F9N+tWnjoYFfeBMra8wYApydElTUYQKP1O7RLHwH1OKFfKcqIA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.969.0", + "@aws-sdk/xml-builder": "3.969.0", + "@smithy/core": "^3.20.6", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/signature-v4": "^5.3.8", + "@smithy/smithy-client": "^4.10.8", + "@smithy/types": "^4.12.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.970.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.970.0.tgz", + "integrity": "sha512-rtVzXzEtAfZBfh+lq3DAvRar4c3jyptweOAJR2DweyXx71QSMY+O879hjpMwES7jl07a3O1zlnFIDo4KP/96kQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.970.0", + "@aws-sdk/types": "3.969.0", + "@smithy/property-provider": "^4.2.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.970.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.970.0.tgz", + "integrity": "sha512-CjDbWL7JxjLc9ZxQilMusWSw05yRvUJKRpz59IxDpWUnSMHC9JMMUUkOy5Izk8UAtzi6gupRWArp4NG4labt9Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.970.0", + "@aws-sdk/types": "3.969.0", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.10.8", + "@smithy/types": "^4.12.0", + "@smithy/util-stream": "^4.5.10", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.971.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.971.0.tgz", + "integrity": "sha512-c0TGJG4xyfTZz3SInXfGU8i5iOFRrLmy4Bo7lMyH+IpngohYMYGYl61omXqf2zdwMbDv+YJ9AviQTcCaEUKi8w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.970.0", + "@aws-sdk/credential-provider-env": "3.970.0", + "@aws-sdk/credential-provider-http": "3.970.0", + "@aws-sdk/credential-provider-login": "3.971.0", + "@aws-sdk/credential-provider-process": "3.970.0", + "@aws-sdk/credential-provider-sso": "3.971.0", + "@aws-sdk/credential-provider-web-identity": "3.971.0", + "@aws-sdk/nested-clients": "3.971.0", + "@aws-sdk/types": "3.969.0", + "@smithy/credential-provider-imds": "^4.2.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/credential-provider-login": { + "version": "3.971.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.971.0.tgz", + "integrity": "sha512-yhbzmDOsk0RXD3rTPhZra4AWVnVAC4nFWbTp+sUty1hrOPurUmhuz8bjpLqYTHGnlMbJp+UqkQONhS2+2LzW2g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.970.0", + "@aws-sdk/nested-clients": "3.971.0", + "@aws-sdk/types": "3.969.0", + "@smithy/property-provider": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.971.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.971.0.tgz", + "integrity": "sha512-epUJBAKivtJqalnEBRsYIULKYV063o/5mXNJshZfyvkAgNIzc27CmmKRXTN4zaNOZg8g/UprFp25BGsi19x3nQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.970.0", + "@aws-sdk/credential-provider-http": "3.970.0", + "@aws-sdk/credential-provider-ini": "3.971.0", + "@aws-sdk/credential-provider-process": "3.970.0", + "@aws-sdk/credential-provider-sso": "3.971.0", + "@aws-sdk/credential-provider-web-identity": "3.971.0", + "@aws-sdk/types": "3.969.0", + "@smithy/credential-provider-imds": "^4.2.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.970.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.970.0.tgz", + "integrity": "sha512-0XeT8OaT9iMA62DFV9+m6mZfJhrD0WNKf4IvsIpj2Z7XbaYfz3CoDDvNoALf3rPY9NzyMHgDxOspmqdvXP00mw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.970.0", + "@aws-sdk/types": "3.969.0", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.971.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.971.0.tgz", + "integrity": "sha512-dY0hMQ7dLVPQNJ8GyqXADxa9w5wNfmukgQniLxGVn+dMRx3YLViMp5ZpTSQpFhCWNF0oKQrYAI5cHhUJU1hETw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.971.0", + "@aws-sdk/core": "3.970.0", + "@aws-sdk/token-providers": "3.971.0", + "@aws-sdk/types": "3.969.0", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.971.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.971.0.tgz", + "integrity": "sha512-F1AwfNLr7H52T640LNON/h34YDiMuIqW/ZreGzhRR6vnFGaSPtNSKAKB2ssAMkLM8EVg8MjEAYD3NCUiEo+t/w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.970.0", + "@aws-sdk/nested-clients": "3.971.0", + "@aws-sdk/types": "3.969.0", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.969.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.969.0.tgz", + "integrity": "sha512-AWa4rVsAfBR4xqm7pybQ8sUNJYnjyP/bJjfAw34qPuh3M9XrfGbAHG0aiAfQGrBnmS28jlO6Kz69o+c6PRw1dw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.969.0", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/middleware-logger": { + "version": "3.969.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.969.0.tgz", + "integrity": "sha512-xwrxfip7Y2iTtCMJ+iifN1E1XMOuhxIHY9DreMCvgdl4r7+48x2S1bCYPWH3eNY85/7CapBWdJ8cerpEl12sQQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.969.0", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.969.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.969.0.tgz", + "integrity": "sha512-2r3PuNquU3CcS1Am4vn/KHFwLi8QFjMdA/R+CRDXT4AFO/0qxevF/YStW3gAKntQIgWgQV8ZdEtKAoJvLI4UWg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.969.0", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.970.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.970.0.tgz", + "integrity": "sha512-dnSJGGUGSFGEX2NzvjwSefH+hmZQ347AwbLhAsi0cdnISSge+pcGfOFrJt2XfBIypwFe27chQhlfuf/gWdzpZg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.970.0", + "@aws-sdk/types": "3.969.0", + "@aws-sdk/util-endpoints": "3.970.0", + "@smithy/core": "^3.20.6", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/nested-clients": { + "version": "3.971.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.971.0.tgz", + "integrity": "sha512-TWaILL8GyYlhGrxxnmbkazM4QsXatwQgoWUvo251FXmUOsiXDFDVX3hoGIfB3CaJhV2pJPfebHUNJtY6TjZ11g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.970.0", + "@aws-sdk/middleware-host-header": "3.969.0", + "@aws-sdk/middleware-logger": "3.969.0", + "@aws-sdk/middleware-recursion-detection": "3.969.0", + "@aws-sdk/middleware-user-agent": "3.970.0", + "@aws-sdk/region-config-resolver": "3.969.0", + "@aws-sdk/types": "3.969.0", + "@aws-sdk/util-endpoints": "3.970.0", + "@aws-sdk/util-user-agent-browser": "3.969.0", + "@aws-sdk/util-user-agent-node": "3.971.0", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.20.6", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.7", + "@smithy/middleware-retry": "^4.4.23", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.10.8", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@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.22", + "@smithy/util-defaults-mode-node": "^4.2.25", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.969.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.969.0.tgz", + "integrity": "sha512-scj9OXqKpcjJ4jsFLtqYWz3IaNvNOQTFFvEY8XMJXTv+3qF5I7/x9SJtKzTRJEBF3spjzBUYPtGFbs9sj4fisQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.969.0", + "@smithy/config-resolver": "^4.4.6", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/token-providers": { + "version": "3.971.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.971.0.tgz", + "integrity": "sha512-4hKGWZbmuDdONMJV0HJ+9jwTDb0zLfKxcCLx2GEnBY31Gt9GeyIQ+DZ97Bb++0voawj6pnZToFikXTyrEq2x+w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.970.0", + "@aws-sdk/nested-clients": "3.971.0", + "@aws-sdk/types": "3.969.0", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/types": { + "version": "3.969.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.969.0.tgz", + "integrity": "sha512-7IIzM5TdiXn+VtgPdVLjmE6uUBUtnga0f4RiSEI1WW10RPuNvZ9U+pL3SwDiRDAdoGrOF9tSLJOFZmfuwYuVYQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/util-endpoints": { + "version": "3.970.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.970.0.tgz", + "integrity": "sha512-TZNZqFcMUtjvhZoZRtpEGQAdULYiy6rcGiXAbLU7e9LSpIYlRqpLa207oMNfgbzlL2PnHko+eVg8rajDiSOYCg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.969.0", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.969.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.969.0.tgz", + "integrity": "sha512-bpJGjuKmFr0rA6UKUCmN8D19HQFMLXMx5hKBXqBlPFdalMhxJSjcxzX9DbQh0Fn6bJtxCguFmRGOBdQqNOt49g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.969.0", + "@smithy/types": "^4.12.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.971.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.971.0.tgz", + "integrity": "sha512-Eygjo9mFzQYjbGY3MYO6CsIhnTwAMd3WmuFalCykqEmj2r5zf0leWrhPaqvA5P68V5JdGfPYgj7vhNOd6CtRBQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.970.0", + "@aws-sdk/types": "3.969.0", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.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.969.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.969.0.tgz", + "integrity": "sha512-BSe4Lx/qdRQQdX8cSSI7Et20vqBspzAjBy8ZmXVoyLkol3y4sXBXzn+BiLtR+oh60ExQn6o2DU4QjdOZbXaKIQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/client-kinesis": { "version": "3.964.0", "license": "Apache-2.0", @@ -5911,10 +6498,12 @@ } }, "node_modules/@smithy/abort-controller": { - "version": "4.2.7", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.8.tgz", + "integrity": "sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.11.0", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -5943,14 +6532,16 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.4.5", + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.6.tgz", + "integrity": "sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.7", - "@smithy/types": "^4.11.0", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", - "@smithy/util-endpoints": "^3.2.7", - "@smithy/util-middleware": "^4.2.7", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" }, "engines": { @@ -5958,16 +6549,18 @@ } }, "node_modules/@smithy/core": { - "version": "3.20.0", + "version": "3.20.7", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.20.7.tgz", + "integrity": "sha512-aO7jmh3CtrmPsIJxUwYIzI5WVlMK8BMCPQ4D4nTzqTqBhbzvxHNzBMGcEg13yg/z9R2Qsz49NUFl0F0lVbTVFw==", "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-serde": "^4.2.8", - "@smithy/protocol-http": "^5.3.7", - "@smithy/types": "^4.11.0", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-middleware": "^4.2.7", - "@smithy/util-stream": "^4.5.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-stream": "^4.5.10", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" @@ -5977,13 +6570,15 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.7", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.8.tgz", + "integrity": "sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.7", - "@smithy/property-provider": "^4.2.7", - "@smithy/types": "^4.11.0", - "@smithy/url-parser": "^4.2.7", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", "tslib": "^2.6.2" }, "engines": { @@ -6051,12 +6646,14 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.8", + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.9.tgz", + "integrity": "sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.7", - "@smithy/querystring-builder": "^4.2.7", - "@smithy/types": "^4.11.0", + "@smithy/protocol-http": "^5.3.8", + "@smithy/querystring-builder": "^4.2.8", + "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" }, @@ -6078,10 +6675,12 @@ } }, "node_modules/@smithy/hash-node": { - "version": "4.2.7", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.8.tgz", + "integrity": "sha512-7ZIlPbmaDGxVoxErDZnuFG18WekhbA/g2/i97wGj+wUBeS6pcUeAym8u4BXh/75RXWhgIJhyC11hBzig6MljwA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.11.0", + "@smithy/types": "^4.12.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" @@ -6103,10 +6702,12 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "4.2.7", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.8.tgz", + "integrity": "sha512-N9iozRybwAQ2dn9Fot9kI6/w9vos2oTXLhtK7ovGqwZjlOcxu6XhPlpLpC+INsxktqHinn5gS2DXDjDF2kG5sQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.11.0", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -6136,11 +6737,13 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "4.2.7", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.8.tgz", + "integrity": "sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.7", - "@smithy/types": "^4.11.0", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -6148,16 +6751,18 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.1", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.20.0", - "@smithy/middleware-serde": "^4.2.8", - "@smithy/node-config-provider": "^4.3.7", - "@smithy/shared-ini-file-loader": "^4.4.2", - "@smithy/types": "^4.11.0", - "@smithy/url-parser": "^4.2.7", - "@smithy/util-middleware": "^4.2.7", + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.8.tgz", + "integrity": "sha512-TV44qwB/T0OMMzjIuI+JeS0ort3bvlPJ8XIH0MSlGADraXpZqmyND27ueuAL3E14optleADWqtd7dUgc2w+qhQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.20.7", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" }, "engines": { @@ -6165,16 +6770,18 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.17", - "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/types": "^4.11.0", - "@smithy/util-middleware": "^4.2.7", - "@smithy/util-retry": "^4.2.7", + "version": "4.4.24", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.24.tgz", + "integrity": "sha512-yiUY1UvnbUFfP5izoKLtfxDSTRv724YRRwyiC/5HYY6vdsVDcDOXKSXmkJl/Hovcxt5r+8tZEUAdrOaCJwrl9Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/service-error-classification": "^4.2.8", + "@smithy/smithy-client": "^4.10.9", + "@smithy/types": "^4.12.0", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" }, @@ -6183,11 +6790,13 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.2.8", + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.9.tgz", + "integrity": "sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.7", - "@smithy/types": "^4.11.0", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -6195,10 +6804,12 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.2.7", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.8.tgz", + "integrity": "sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.11.0", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -6206,12 +6817,14 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.3.7", + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.8.tgz", + "integrity": "sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.7", - "@smithy/shared-ini-file-loader": "^4.4.2", - "@smithy/types": "^4.11.0", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -6219,13 +6832,15 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.4.7", + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.8.tgz", + "integrity": "sha512-q9u+MSbJVIJ1QmJ4+1u+cERXkrhuILCBDsJUBAW1MPE6sFonbCNaegFuwW9ll8kh5UdyY3jOkoOGlc7BesoLpg==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.7", - "@smithy/protocol-http": "^5.3.7", - "@smithy/querystring-builder": "^4.2.7", - "@smithy/types": "^4.11.0", + "@smithy/abort-controller": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/querystring-builder": "^4.2.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -6233,10 +6848,12 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.2.7", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.8.tgz", + "integrity": "sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.11.0", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -6244,10 +6861,12 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.3.7", + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.8.tgz", + "integrity": "sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.11.0", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -6255,10 +6874,12 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.2.7", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.8.tgz", + "integrity": "sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.11.0", + "@smithy/types": "^4.12.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" }, @@ -6267,10 +6888,12 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.2.7", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.8.tgz", + "integrity": "sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.11.0", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -6278,20 +6901,24 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.2.7", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.8.tgz", + "integrity": "sha512-mZ5xddodpJhEt3RkCjbmUQuXUOaPNTkbMGR0bcS8FE0bJDLMZlhmpgrvPNCYglVw5rsYTpSnv19womw9WWXKQQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.11.0" + "@smithy/types": "^4.12.0" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.4.2", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.3.tgz", + "integrity": "sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.11.0", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -6299,14 +6926,16 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.7", + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.8.tgz", + "integrity": "sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.2.0", - "@smithy/protocol-http": "^5.3.7", - "@smithy/types": "^4.11.0", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", - "@smithy/util-middleware": "^4.2.7", + "@smithy/util-middleware": "^4.2.8", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" @@ -6316,15 +6945,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.10.2", + "version": "4.10.9", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.10.9.tgz", + "integrity": "sha512-Je0EvGXVJ0Vrrr2lsubq43JGRIluJ/hX17aN/W/A0WfE+JpoMdI8kwk2t9F0zTX9232sJDGcoH4zZre6m6f/sg==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.20.0", - "@smithy/middleware-endpoint": "^4.4.1", - "@smithy/middleware-stack": "^4.2.7", - "@smithy/protocol-http": "^5.3.7", - "@smithy/types": "^4.11.0", - "@smithy/util-stream": "^4.5.8", + "@smithy/core": "^3.20.7", + "@smithy/middleware-endpoint": "^4.4.8", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "@smithy/util-stream": "^4.5.10", "tslib": "^2.6.2" }, "engines": { @@ -6332,7 +6963,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.11.0", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.12.0.tgz", + "integrity": "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -6342,11 +6975,13 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.2.7", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.8.tgz", + "integrity": "sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA==", "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^4.2.7", - "@smithy/types": "^4.11.0", + "@smithy/querystring-parser": "^4.2.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -6407,12 +7042,14 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.16", + "version": "4.3.23", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.23.tgz", + "integrity": "sha512-mMg+r/qDfjfF/0psMbV4zd7F/i+rpyp7Hjh0Wry7eY15UnzTEId+xmQTGDU8IdZtDfbGQxuWNfgBZKBj+WuYbA==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.7", - "@smithy/smithy-client": "^4.10.2", - "@smithy/types": "^4.11.0", + "@smithy/property-provider": "^4.2.8", + "@smithy/smithy-client": "^4.10.9", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -6420,15 +7057,17 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.19", + "version": "4.2.26", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.26.tgz", + "integrity": "sha512-EQqe/WkbCinah0h1lMWh9ICl0Ob4lyl20/10WTB35SC9vDQfD8zWsOT+x2FIOXKAoZQ8z/y0EFMoodbcqWJY/w==", "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/types": "^4.11.0", + "@smithy/config-resolver": "^4.4.6", + "@smithy/credential-provider-imds": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/smithy-client": "^4.10.9", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -6436,11 +7075,13 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.2.7", + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.8.tgz", + "integrity": "sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.7", - "@smithy/types": "^4.11.0", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -6458,10 +7099,12 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.2.7", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.8.tgz", + "integrity": "sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.11.0", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -6469,11 +7112,13 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.2.7", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.8.tgz", + "integrity": "sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.2.7", - "@smithy/types": "^4.11.0", + "@smithy/service-error-classification": "^4.2.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -6481,12 +7126,14 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.5.8", + "version": "4.5.10", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.10.tgz", + "integrity": "sha512-jbqemy51UFSZSp2y0ZmRfckmrzuKww95zT9BYMmuJ8v3altGcqjwoV1tzpOwuHaKrwQrCjIzOib499ymr2f98g==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.3.8", - "@smithy/node-http-handler": "^4.4.7", - "@smithy/types": "^4.11.0", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", @@ -14552,6 +15199,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 @@ -14584,6 +15235,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", "license": "MIT", From f33c999749d0c443504a43e6308ddaabfa3ee928 Mon Sep 17 00:00:00 2001 From: Steve Buxton Date: Tue, 20 Jan 2026 09:54:29 +0000 Subject: [PATCH 2/3] Place SNS records on queue --- .../src/__tests__/allocator.test.ts | 18 +++++++-------- lambdas/allocation/src/allocator.ts | 22 ++++++++----------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/lambdas/allocation/src/__tests__/allocator.test.ts b/lambdas/allocation/src/__tests__/allocator.test.ts index c57d08ba..d2c01918 100644 --- a/lambdas/allocation/src/__tests__/allocator.test.ts +++ b/lambdas/allocation/src/__tests__/allocator.test.ts @@ -79,7 +79,7 @@ describe("allocator", () => { }); describe("createAllocator", () => { - it("should process a single SNS record and send message to SQS", async () => { + it("should place the SNS event unchanged on the SQS queue", async () => { const letterEvent = createLetterEvent("id1"); const snsEvent = createSNSEvent([ createSNSEventRecord(JSON.stringify(letterEvent)), @@ -92,8 +92,8 @@ describe("allocator", () => { expect.objectContaining({ input: { QueueUrl: mockQueueUrl, - MessageBody: JSON.stringify(letterEvent), - MessageGroupId: "id1", + MessageBody: JSON.stringify(snsEvent.Records[0]), + MessageGroupId: expect.any(String), }, }), ); @@ -120,8 +120,8 @@ describe("allocator", () => { expect.objectContaining({ input: { QueueUrl: mockQueueUrl, - MessageBody: JSON.stringify(letterEvent1), - MessageGroupId: "id1", + MessageBody: JSON.stringify(snsEvent.Records[0]), + MessageGroupId: expect.any(String), }, }), ); @@ -130,8 +130,8 @@ describe("allocator", () => { expect.objectContaining({ input: { QueueUrl: mockQueueUrl, - MessageBody: JSON.stringify(letterEvent2), - MessageGroupId: "id2", + MessageBody: JSON.stringify(snsEvent.Records[1]), + MessageGroupId: expect.any(String), }, }), ); @@ -140,8 +140,8 @@ describe("allocator", () => { expect.objectContaining({ input: { QueueUrl: mockQueueUrl, - MessageBody: JSON.stringify(letterEvent3), - MessageGroupId: "id3", + MessageBody: JSON.stringify(snsEvent.Records[2]), + MessageGroupId: expect.any(String), }, }), ); diff --git a/lambdas/allocation/src/allocator.ts b/lambdas/allocation/src/allocator.ts index 138fe1a6..5c324ab6 100644 --- a/lambdas/allocation/src/allocator.ts +++ b/lambdas/allocation/src/allocator.ts @@ -1,9 +1,7 @@ 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 { randomUUID } from "node:crypto"; + import { Deps } from "./deps"; export default function createAllocator(deps: Deps): SNSHandler { @@ -11,8 +9,8 @@ export default function createAllocator(deps: Deps): SNSHandler { // 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)); + buildSendMessageCommand(record, deps.queueUrl), + ); for (const sqsCommand of sqsCommands) { deps.logger.info({ @@ -24,14 +22,12 @@ export default function createAllocator(deps: Deps): SNSHandler { }; } -function extractLetterEvent(record: SNSEventRecord): LetterEvent { - return $LetterEvent.parse(JSON.parse(record.Sns.Message)); -} - -function buildSendMessageCommand(letterEvent: LetterEvent, queueUrl: string) { +function buildSendMessageCommand(snsRecord: SNSEventRecord, queueUrl: string) { return new SendMessageCommand({ QueueUrl: queueUrl, - MessageBody: JSON.stringify(letterEvent), - MessageGroupId: letterEvent.data.domainId, + MessageBody: JSON.stringify(snsRecord), + // Using a random UUID here effectively means that the amendments queue is not FIFO for new pending + // letters. Pragmatically this is OK because we shouldn't be getting updates from the supplier yet. + MessageGroupId: randomUUID(), }); } From 5f34443a4d2ac70de525b535d623eb26a42966e0 Mon Sep 17 00:00:00 2001 From: Steve Buxton Date: Tue, 20 Jan 2026 11:29:11 +0000 Subject: [PATCH 3/3] Use SNS message, not event --- lambdas/allocation/src/__tests__/allocator.test.ts | 8 ++++---- lambdas/allocation/src/allocator.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lambdas/allocation/src/__tests__/allocator.test.ts b/lambdas/allocation/src/__tests__/allocator.test.ts index d2c01918..77dc9b92 100644 --- a/lambdas/allocation/src/__tests__/allocator.test.ts +++ b/lambdas/allocation/src/__tests__/allocator.test.ts @@ -92,7 +92,7 @@ describe("allocator", () => { expect.objectContaining({ input: { QueueUrl: mockQueueUrl, - MessageBody: JSON.stringify(snsEvent.Records[0]), + MessageBody: JSON.stringify(snsEvent.Records[0].Sns), MessageGroupId: expect.any(String), }, }), @@ -120,7 +120,7 @@ describe("allocator", () => { expect.objectContaining({ input: { QueueUrl: mockQueueUrl, - MessageBody: JSON.stringify(snsEvent.Records[0]), + MessageBody: JSON.stringify(snsEvent.Records[0].Sns), MessageGroupId: expect.any(String), }, }), @@ -130,7 +130,7 @@ describe("allocator", () => { expect.objectContaining({ input: { QueueUrl: mockQueueUrl, - MessageBody: JSON.stringify(snsEvent.Records[1]), + MessageBody: JSON.stringify(snsEvent.Records[1].Sns), MessageGroupId: expect.any(String), }, }), @@ -140,7 +140,7 @@ describe("allocator", () => { expect.objectContaining({ input: { QueueUrl: mockQueueUrl, - MessageBody: JSON.stringify(snsEvent.Records[2]), + MessageBody: JSON.stringify(snsEvent.Records[2].Sns), MessageGroupId: expect.any(String), }, }), diff --git a/lambdas/allocation/src/allocator.ts b/lambdas/allocation/src/allocator.ts index 5c324ab6..e4d45285 100644 --- a/lambdas/allocation/src/allocator.ts +++ b/lambdas/allocation/src/allocator.ts @@ -25,7 +25,7 @@ export default function createAllocator(deps: Deps): SNSHandler { function buildSendMessageCommand(snsRecord: SNSEventRecord, queueUrl: string) { return new SendMessageCommand({ QueueUrl: queueUrl, - MessageBody: JSON.stringify(snsRecord), + MessageBody: JSON.stringify(snsRecord.Sns), // Using a random UUID here effectively means that the amendments queue is not FIFO for new pending // letters. Pragmatically this is OK because we shouldn't be getting updates from the supplier yet. MessageGroupId: randomUUID(),