diff --git a/.trivyignore b/.trivyignore new file mode 100644 index 0000000..5135cec --- /dev/null +++ b/.trivyignore @@ -0,0 +1,18 @@ +# Add CVE IDs to ignore specific vulnerabilities. +# Keep justification inline after the CVE for auditability. +# Syntax: one entry per line, comments allowed. + +# Examples: +# CVE-2025-0001 # Unexploitable in AWS Lambda base per vendor advisory +# CVE-2024-12345 # False positive: not present in runtime layer +# CVE-2024-12345 # https://avd.aquasec.com/nvd/cve-2024-12345 - package-name - < 2.0.1 - justification + +########################### +# Package Vulnerabilities # +########################### + +# All CVEs below are tracked for remediation under the following Jira ticket: +# https://nhsd-jira.digital.nhs.uk/browse/CCM-14687 +# EXAMPLE: +# CVE-2024-12345 # https://avd.aquasec.com/nvd/cve-2024-12345 - package-name - < 2.0.1 - justification +CVE-2026-26996 https://avd.aquasec.com/nvd/cve-2026-26996 - minimatch - <10.2.1 - This is a dev dependency used in the build process, not present in the runtime layer, and therefore not exploitable in production. We will update to a non-vulnerable versions for our transitive dependencies when available. diff --git a/.vscode/settings.json b/.vscode/settings.json index 5ad1f2c..317777d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,4 @@ { - "autoOpenWorkspace.enableAutoOpenIfSingleWorkspace": true, "files.exclude": { "**/.DS_Store": true, "**/.git": true, @@ -11,5 +10,10 @@ ".devcontainer": true, ".github": false, ".vscode": false + }, + "jest.jestCommandLine": "npm run test:unit --workspaces --", + "sonarlint.connectedMode.project": { + "connectionId": "nhsdigital", + "projectKey": "NHSDigital_nhs-notify-client-callbacks" } } diff --git a/AGENTS.md b/AGENTS.md index c85e1af..982ca63 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -81,7 +81,7 @@ When proposing a change, agents should: to catch formatting and basic lint issues. Domain specific checks will be defined in appropriate nested AGENTS.md files. -- Suggest at least one extra validation step (for example `npm test` in a lambda, or triggering a specific workflow). +- Suggest at least one extra validation step (for example `npm test:unit` in a lambda, or triggering a specific workflow). - Any required follow up activites which fall outside of the current task's scope should be clearly marked with a 'TODO: CCM-12345' comment. The human user should be prompted to create and provide a JIRA ticket ID to be added to the comment. ## Security & Safety @@ -93,3 +93,11 @@ When proposing a change, agents should: ## Escalation / Blockers If you are blocked by an unavailable secret, unclear architectural constraint, missing upstream module, or failing tooling you cannot safely fix, stop and ask a single clear clarifying question rather than guessing. + +## Comment Policy + +- No JSDoc unless it's a public API with non-obvious behavior +- No inline comments that just describe what the next line does +- Only comment when explaining WHY, not WHAT +- Prefer better naming over comments +- Trust developers can read TypeScript diff --git a/eslint.config.mjs b/eslint.config.mjs index 957b983..b5b83a3 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -229,6 +229,13 @@ export default defineConfig([ "no-relative-import-paths/no-relative-import-paths": 2, }, }, + { + files: ["**/jest.config.ts"], + rules: { + "no-relative-import-paths/no-relative-import-paths": 0, + "import-x/no-relative-packages": 0, + }, + }, { files: ["scripts/**"], rules: { diff --git a/infrastructure/terraform/components/callbacks/README.md b/infrastructure/terraform/components/callbacks/README.md index d69eaeb..967c98a 100644 --- a/infrastructure/terraform/components/callbacks/README.md +++ b/infrastructure/terraform/components/callbacks/README.md @@ -16,6 +16,7 @@ | [clients](#input\_clients) | n/a |
list(object({
connection_name = string
destination_name = string
invocation_endpoint = string
invocation_rate_limit_per_second = optional(number, 10)
http_method = optional(string, "POST")
header_name = optional(string, "x-api-key")
header_value = string
client_detail = list(string)
}))
| `[]` | no | | [component](#input\_component) | The variable encapsulating the name of this component | `string` | `"callbacks"` | no | | [default\_tags](#input\_default\_tags) | A map of default tags to apply to all taggable resources within the component | `map(string)` | `{}` | no | +| [deploy\_mock\_webhook](#input\_deploy\_mock\_webhook) | Flag to deploy mock webhook lambda for integration testing (test/dev environments only) | `bool` | `true` | no | | [environment](#input\_environment) | The name of the tfscaffold environment | `string` | n/a | yes | | [force\_lambda\_code\_deploy](#input\_force\_lambda\_code\_deploy) | If the lambda package in s3 has the same commit id tag as the terraform build branch, the lambda will not update automatically. Set to True if making changes to Lambda code from on the same commit for example during development | `bool` | `false` | no | | [group](#input\_group) | The group variables are being inherited from (often synonmous with account short-name) | `string` | n/a | yes | @@ -24,6 +25,7 @@ | [log\_retention\_in\_days](#input\_log\_retention\_in\_days) | The retention period in days for the Cloudwatch Logs events to be retained, default of 0 is indefinite | `number` | `0` | no | | [parent\_acct\_environment](#input\_parent\_acct\_environment) | Name of the environment responsible for the acct resources used, affects things like DNS zone. Useful for named dev environments | `string` | `"main"` | no | | [pipe\_event\_patterns](#input\_pipe\_event\_patterns) | value | `list(string)` | `[]` | no | +| [pipe\_log\_level](#input\_pipe\_log\_level) | Log level for the EventBridge Pipe. | `string` | `"INFO"` | no | | [pipe\_sqs\_input\_batch\_size](#input\_pipe\_sqs\_input\_batch\_size) | n/a | `number` | `1` | no | | [pipe\_sqs\_max\_batch\_window](#input\_pipe\_sqs\_max\_batch\_window) | n/a | `number` | `2` | no | | [project](#input\_project) | The name of the tfscaffold project | `string` | n/a | yes | @@ -32,13 +34,18 @@ | Name | Source | Version | |------|--------|---------| +| [client\_config\_bucket](#module\_client\_config\_bucket) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.28/terraform-s3bucket.zip | n/a | | [client\_destination](#module\_client\_destination) | ../../modules/client-destination | n/a | | [client\_transform\_filter\_lambda](#module\_client\_transform\_filter\_lambda) | git::https://github.com/NHSDigital/nhs-notify-shared-modules.git//infrastructure/modules/lambda | v2.0.29 | | [kms](#module\_kms) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-kms.zip | n/a | +| [mock\_webhook\_lambda](#module\_mock\_webhook\_lambda) | git::https://github.com/NHSDigital/nhs-notify-shared-modules.git//infrastructure/modules/lambda | v2.0.29 | | [sqs\_inbound\_event](#module\_sqs\_inbound\_event) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-sqs.zip | n/a | ## Outputs -No outputs. +| Name | Description | +|------|-------------| +| [mock\_webhook\_lambda\_log\_group\_name](#output\_mock\_webhook\_lambda\_log\_group\_name) | CloudWatch log group name for mock webhook lambda (for integration test queries) | +| [mock\_webhook\_url](#output\_mock\_webhook\_url) | URL endpoint for mock webhook (for TEST\_WEBHOOK\_URL environment variable) | diff --git a/infrastructure/terraform/components/callbacks/locals.tf b/infrastructure/terraform/components/callbacks/locals.tf index ceb8acb..dd068e1 100644 --- a/infrastructure/terraform/components/callbacks/locals.tf +++ b/infrastructure/terraform/components/callbacks/locals.tf @@ -4,8 +4,29 @@ locals { root_domain_name = "${var.environment}.${local.acct.route53_zone_names["client-callbacks"]}" # e.g. [main|dev|abxy0].smsnudge.[dev|nonprod|prod].nhsnotify.national.nhs.uk root_domain_id = local.acct.route53_zone_ids["client-callbacks"] + # Clients from variable clients_by_name = { for client in var.clients : client.connection_name => client } + + # Automatic test client when mock webhook is deployed + test_client = var.deploy_mock_webhook ? { + "test-client" = { + connection_name = "test-client" + destination_name = "test-destination" + invocation_endpoint = aws_lambda_function_url.mock_webhook[0].function_url + invocation_rate_limit_per_second = 10 + http_method = "POST" + header_name = "x-api-key" + header_value = "test-api-key-placeholder" + client_detail = [ + "uk.nhs.notify.client-callbacks.message.status.transitioned.v1", + "uk.nhs.notify.client-callbacks.channel.status.transitioned.v1" + ] + } + } : {} + + # Merge configured clients with test client + all_clients = merge(local.clients_by_name, local.test_client) } diff --git a/infrastructure/terraform/components/callbacks/module_client_destination.tf b/infrastructure/terraform/components/callbacks/module_client_destination.tf index 59c7765..19f3c12 100644 --- a/infrastructure/terraform/components/callbacks/module_client_destination.tf +++ b/infrastructure/terraform/components/callbacks/module_client_destination.tf @@ -1,6 +1,6 @@ module "client_destination" { source = "../../modules/client-destination" - for_each = local.clients_by_name + for_each = local.all_clients project = var.project aws_account_id = var.aws_account_id diff --git a/infrastructure/terraform/components/callbacks/module_kms.tf b/infrastructure/terraform/components/callbacks/module_kms.tf index ee4c828..319e9a0 100644 --- a/infrastructure/terraform/components/callbacks/module_kms.tf +++ b/infrastructure/terraform/components/callbacks/module_kms.tf @@ -71,7 +71,7 @@ data "aws_iam_policy_document" "kms" { variable = "kms:EncryptionContext:aws:sqs:arn" values = [ "arn:aws:sqs:${var.region}:${var.aws_account_id}:${var.project}-${var.environment}-callbacks-inbound-event-queue", - "arn:aws:sqs:${var.region}:${var.aws_account_id}:${var.project}-${var.environment}-callbacks-*-dlq" #wildcard here so that DLQs for clients can also use this key + "arn:aws:sqs:${var.region}:${var.aws_account_id}:${var.project}-${var.environment}-callbacks-*-dlq-queue" #wildcard here so that DLQs for clients can also use this key ] } } diff --git a/infrastructure/terraform/components/callbacks/module_mock_webhook_lambda.tf b/infrastructure/terraform/components/callbacks/module_mock_webhook_lambda.tf new file mode 100644 index 0000000..9c6e616 --- /dev/null +++ b/infrastructure/terraform/components/callbacks/module_mock_webhook_lambda.tf @@ -0,0 +1,93 @@ +module "mock_webhook_lambda" { + count = var.deploy_mock_webhook ? 1 : 0 + source = "git::https://github.com/NHSDigital/nhs-notify-shared-modules.git//infrastructure/modules/lambda?ref=v2.0.29" + + function_name = "mock-webhook" + description = "Mock webhook endpoint for integration testing - logs received callbacks to CloudWatch" + + 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.mock_webhook_lambda[0].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 = "mock-webhook-lambda/dist" + function_include_common = true + handler_function_name = "handler" + runtime = "nodejs22.x" + memory = 256 + timeout = 10 + log_level = var.log_level + + force_lambda_code_deploy = var.force_lambda_code_deploy + enable_lambda_insights = false + + log_destination_arn = local.log_destination_arn + log_subscription_role_arn = local.acct.log_subscription_role_arn + + lambda_env_vars = { + LOG_LEVEL = var.log_level + } +} + +data "aws_iam_policy_document" "mock_webhook_lambda" { + count = var.deploy_mock_webhook ? 1 : 0 + + statement { + sid = "KMSPermissions" + effect = "Allow" + + actions = [ + "kms:Decrypt", + "kms:GenerateDataKey", + ] + + resources = [ + module.kms.key_arn, + ] + } + + # Mock webhook only needs CloudWatch Logs permissions (already granted by shared lambda module) + # No additional permissions required beyond base Lambda execution role +} + +# Lambda Function URL for mock webhook (test/dev only) +resource "aws_lambda_function_url" "mock_webhook" { + count = var.deploy_mock_webhook ? 1 : 0 + function_name = module.mock_webhook_lambda[0].function_name + authorization_type = "NONE" # Public endpoint for testing + + cors { + allow_origins = ["*"] + allow_methods = ["POST"] + allow_headers = ["*"] + max_age = 86400 + } +} + +resource "aws_lambda_permission" "mock_webhook_function_url" { + count = var.deploy_mock_webhook ? 1 : 0 + statement_id = "FunctionURLAllowPublicAccess" + action = "lambda:InvokeFunctionUrl" + function_name = module.mock_webhook_lambda[0].function_name + principal = "*" + function_url_auth_type = "NONE" +} + +resource "aws_lambda_permission" "mock_webhook_function_invoke" { + count = var.deploy_mock_webhook ? 1 : 0 + statement_id = "FunctionURLAllowInvokeAction" + action = "lambda:InvokeFunction" + function_name = module.mock_webhook_lambda[0].function_name + principal = "*" +} diff --git a/infrastructure/terraform/components/callbacks/module_transform_filter_lambda.tf b/infrastructure/terraform/components/callbacks/module_transform_filter_lambda.tf index c0ce5da..7be32cb 100644 --- a/infrastructure/terraform/components/callbacks/module_transform_filter_lambda.tf +++ b/infrastructure/terraform/components/callbacks/module_transform_filter_lambda.tf @@ -15,7 +15,7 @@ module "client_transform_filter_lambda" { kms_key_arn = module.kms.key_arn ## Requires shared kms module iam_policy_document = { - body = data.aws_iam_policy_document.example_lambda.json + body = data.aws_iam_policy_document.client_transform_filter_lambda.json } function_s3_bucket = local.acct.s3_buckets["lambda_function_artefacts"]["id"] @@ -35,10 +35,12 @@ module "client_transform_filter_lambda" { log_subscription_role_arn = local.acct.log_subscription_role_arn lambda_env_vars = { + ENVIRONMENT = var.environment + METRICS_NAMESPACE = "nhs-notify-client-callbacks-metrics" } } -data "aws_iam_policy_document" "example_lambda" { +data "aws_iam_policy_document" "client_transform_filter_lambda" { statement { sid = "KMSPermissions" effect = "Allow" @@ -52,4 +54,30 @@ data "aws_iam_policy_document" "example_lambda" { module.kms.key_arn, ## Requires shared kms module ] } + + statement { + sid = "S3ClientConfigReadAccess" + effect = "Allow" + + actions = [ + "s3:GetObject", + ] + + resources = [ + "${module.client_config_bucket.arn}/*", + ] + } + + statement { + sid = "CloudWatchMetrics" + effect = "Allow" + + actions = [ + "cloudwatch:PutMetricData", + ] + + resources = [ + "*", + ] + } } diff --git a/infrastructure/terraform/components/callbacks/outputs.tf b/infrastructure/terraform/components/callbacks/outputs.tf index 9dcc2f3..d40c156 100644 --- a/infrastructure/terraform/components/callbacks/outputs.tf +++ b/infrastructure/terraform/components/callbacks/outputs.tf @@ -1 +1,15 @@ # Define the outputs for the component. The outputs may well be referenced by other component in the same or different environments using terraform_remote_state data sources... + +## +# Mock Webhook Lambda Outputs (test/dev environments only) +## + +output "mock_webhook_lambda_log_group_name" { + description = "CloudWatch log group name for mock webhook lambda (for integration test queries)" + value = var.deploy_mock_webhook ? module.mock_webhook_lambda[0].cloudwatch_log_group_name : null +} + +output "mock_webhook_url" { + description = "URL endpoint for mock webhook (for TEST_WEBHOOK_URL environment variable)" + value = var.deploy_mock_webhook ? aws_lambda_function_url.mock_webhook[0].function_url : null +} diff --git a/infrastructure/terraform/components/callbacks/pipes_pipe_main.tf b/infrastructure/terraform/components/callbacks/pipes_pipe_main.tf index e863f8b..e3c284e 100644 --- a/infrastructure/terraform/components/callbacks/pipes_pipe_main.tf +++ b/infrastructure/terraform/components/callbacks/pipes_pipe_main.tf @@ -6,7 +6,7 @@ resource "aws_pipes_pipe" "main" { enrichment = module.client_transform_filter_lambda.function_arn kms_key_identifier = module.kms.key_arn log_configuration { - level = "ERROR" + level = var.pipe_log_level cloudwatch_logs_log_destination { log_group_arn = aws_cloudwatch_log_group.main_pipe.arn } @@ -25,8 +25,8 @@ resource "aws_pipes_pipe" "main" { input_template = <, - "type": <$.body.type> + "type": <$.type>, + "transformedPayload": <$.transformedPayload> } EOF } diff --git a/infrastructure/terraform/components/callbacks/s3_bucket_client_config.tf b/infrastructure/terraform/components/callbacks/s3_bucket_client_config.tf new file mode 100644 index 0000000..9f468ea --- /dev/null +++ b/infrastructure/terraform/components/callbacks/s3_bucket_client_config.tf @@ -0,0 +1,86 @@ +## +# S3 Bucket for Client Subscription Configuration +# +# Storage location for client subscription configurations loaded by Transform & Filter Lambda. +# Files are named {clientId}.json and contain ClientSubscriptionConfiguration arrays. +## + +module "client_config_bucket" { + source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.28/terraform-s3bucket.zip" + + name = "subscription-config" + + aws_account_id = var.aws_account_id + component = var.component + environment = var.environment + project = var.project + region = var.region + + default_tags = merge( + local.default_tags, + { + Description = "Client subscription configuration storage" + } + ) + + kms_key_arn = module.kms.key_arn + force_destroy = false + versioning = true + object_ownership = "BucketOwnerPreferred" + bucket_key_enabled = true + + policy_documents = [ + data.aws_iam_policy_document.client_config_bucket.json + ] +} + +## +# S3 Bucket Policy +# +# Allows Transform & Filter Lambda to read configuration files +## + +data "aws_iam_policy_document" "client_config_bucket" { + statement { + sid = "AllowLambdaReadAccess" + effect = "Allow" + + principals { + type = "AWS" + identifiers = [module.client_transform_filter_lambda.iam_role_arn] + } + + actions = [ + "s3:GetObject", + ] + + resources = [ + "${module.client_config_bucket.arn}/*", + ] + } + + statement { + sid = "DenyInsecureTransport" + effect = "Deny" + + principals { + type = "*" + identifiers = ["*"] + } + + actions = [ + "s3:*", + ] + + resources = [ + module.client_config_bucket.arn, + "${module.client_config_bucket.arn}/*" + ] + + condition { + test = "Bool" + variable = "aws:SecureTransport" + values = ["false"] + } + } +} diff --git a/infrastructure/terraform/components/callbacks/variables.tf b/infrastructure/terraform/components/callbacks/variables.tf index e97d0be..37d4249 100644 --- a/infrastructure/terraform/components/callbacks/variables.tf +++ b/infrastructure/terraform/components/callbacks/variables.tf @@ -103,6 +103,17 @@ variable "clients" { } +variable "pipe_log_level" { + type = string + description = "Log level for the EventBridge Pipe." + default = "INFO" // TODO: CCM-14200 - revert + + validation { + condition = contains(["OFF", "ERROR", "INFO", "TRACE"], var.pipe_log_level) + error_message = "pipe_log_level must be one of: OFF, ERROR, INFO, TRACE." + } +} + variable "pipe_sqs_input_batch_size" { type = number default = 1 @@ -112,3 +123,9 @@ variable "pipe_sqs_max_batch_window" { type = number default = 2 } + +variable "deploy_mock_webhook" { + type = bool + description = "Flag to deploy mock webhook lambda for integration testing (test/dev environments only)" + default = true # CCM-14200: Temporary test value, revert to false +} diff --git a/infrastructure/terraform/modules/client-destination/cloudwatch_event_rule_main.tf b/infrastructure/terraform/modules/client-destination/cloudwatch_event_rule_main.tf index ca8403b..a2bcd19 100644 --- a/infrastructure/terraform/modules/client-destination/cloudwatch_event_rule_main.tf +++ b/infrastructure/terraform/modules/client-destination/cloudwatch_event_rule_main.tf @@ -5,10 +5,7 @@ resource "aws_cloudwatch_event_rule" "main" { event_pattern = jsonencode({ "detail" : { - "type" : var.client_detail, - "dataschemaversion" : [{ - "prefix" : "1." - }] + "type" : var.client_detail } }) } @@ -19,6 +16,7 @@ resource "aws_cloudwatch_event_target" "main" { target_id = "${local.csi}-${var.connection_name}" role_arn = aws_iam_role.api_target_role.arn event_bus_name = var.client_bus_name + input_path = "$.detail.transformedPayload" dead_letter_config { arn = module.target_dlq.sqs_queue_arn diff --git a/infrastructure/terraform/modules/client-destination/module_target_dlq.tf b/infrastructure/terraform/modules/client-destination/module_target_dlq.tf index 62ce68a..fe302a5 100644 --- a/infrastructure/terraform/modules/client-destination/module_target_dlq.tf +++ b/infrastructure/terraform/modules/client-destination/module_target_dlq.tf @@ -32,7 +32,7 @@ data "aws_iam_policy_document" "target_dlq" { ] resources = [ - "arn:aws:sqs:${var.region}:${var.aws_account_id}:${var.project}-${var.environment}-${var.component}-${var.connection_name}-dlq" + "arn:aws:sqs:${var.region}:${var.aws_account_id}:${var.project}-${var.environment}-${var.component}-${var.connection_name}-dlq-queue" ] } } diff --git a/jest.config.base.ts b/jest.config.base.ts new file mode 100644 index 0000000..837591f --- /dev/null +++ b/jest.config.base.ts @@ -0,0 +1,30 @@ +import type { Config } from "jest"; + +export const baseJestConfig: Config = { + preset: "ts-jest", + clearMocks: true, + collectCoverage: true, + coverageDirectory: "./.reports/unit/coverage", + coverageProvider: "babel", + coveragePathIgnorePatterns: ["/__tests__/"], + transform: { "^.+\\.ts$": "ts-jest" }, + testPathIgnorePatterns: [".build"], + testMatch: ["**/?(*.)+(spec|test).[jt]s?(x)"], + reporters: [ + "default", + [ + "jest-html-reporter", + { + pageTitle: "Test Report", + outputPath: "./.reports/unit/test-report.html", + includeFailureMsg: true, + }, + ], + ], +}; + +export const nodeJestConfig: Config = { + ...baseJestConfig, + testEnvironment: "node", + modulePaths: ["/src"], +}; diff --git a/lambdas/client-transform-filter-lambda/jest.config.ts b/lambdas/client-transform-filter-lambda/jest.config.ts index f88e727..438663c 100644 --- a/lambdas/client-transform-filter-lambda/jest.config.ts +++ b/lambdas/client-transform-filter-lambda/jest.config.ts @@ -1,60 +1,17 @@ -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", +import { nodeJestConfig } from "../../jest.config.base"; +export default { + ...nodeJestConfig, coverageThreshold: { global: { - branches: 100, - functions: 100, - lines: 100, - statements: -10, + branches: 50, + functions: 50, + lines: 50, + statements: -50, }, }, - - 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 ?? []), + ...(nodeJestConfig.coveragePathIgnorePatterns ?? []), "zod-validators.ts", ], }; - -export default utilsJestConfig; diff --git a/lambdas/client-transform-filter-lambda/package.json b/lambdas/client-transform-filter-lambda/package.json index f288265..e2482f9 100644 --- a/lambdas/client-transform-filter-lambda/package.json +++ b/lambdas/client-transform-filter-lambda/package.json @@ -1,6 +1,11 @@ { "dependencies": { - "esbuild": "^0.25.0" + "aws-embedded-metrics": "^4.2.1", + "cloudevents": "^8.0.2", + "esbuild": "^0.25.0", + "p-map": "^4.0.0", + "pino": "^9.6.0", + "zod": "^4.1.13" }, "devDependencies": { "@tsconfig/node22": "^22.0.2", diff --git a/lambdas/client-transform-filter-lambda/src/__tests__/index.test.ts b/lambdas/client-transform-filter-lambda/src/__tests__/index.test.ts index b00cc1c..518bbb6 100644 --- a/lambdas/client-transform-filter-lambda/src/__tests__/index.test.ts +++ b/lambdas/client-transform-filter-lambda/src/__tests__/index.test.ts @@ -1,77 +1,445 @@ -import { handler } from ".."; +import type { SQSRecord } from "aws-lambda"; +import type { MetricsLogger } from "aws-embedded-metrics"; +import type { StatusTransitionEvent } from "models/status-transition-event"; +import type { MessageStatusData } from "models/message-status-data"; +import type { ChannelStatusData } from "models/channel-status-data"; +import type { + ChannelStatusAttributes, + MessageStatusAttributes, +} from "models/client-callback-payload"; +import type { Logger } from "services/logger"; +import type { CallbackMetrics } from "services/metrics"; +import { ObservabilityService } from "services/observability"; +import { createHandler } from ".."; describe("Lambda handler", () => { - it("extracts from a stringified event", async () => { - const eventStr = JSON.stringify({ - body: { - dataschemaversion: "1.0", - type: "uk.nhs.notify.client-callbacks.test-sid", + const mockLogger = { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + child: jest.fn(), + } as unknown as Logger; + + (mockLogger.child as jest.Mock).mockImplementation(() => mockLogger); + + const mockMetrics = { + emitEventReceived: jest.fn(), + emitTransformationSuccess: jest.fn(), + emitTransformationFailure: jest.fn(), + emitDeliveryInitiated: jest.fn(), + emitValidationError: jest.fn(), + } as unknown as CallbackMetrics; + + const mockMetricsLogger = { + flush: jest.fn().mockImplementation(async () => {}), + } as unknown as MetricsLogger; + + const handler = createHandler({ + createObservabilityService: () => + new ObservabilityService(mockLogger, mockMetrics, mockMetricsLogger), + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const validMessageStatusEvent: StatusTransitionEvent = { + specversion: "1.0", + id: "661f9510-f39c-52e5-b827-557766551111", + source: + "/nhs/england/notify/development/primary/data-plane/client-callbacks", + subject: + "customer/920fca11-596a-4eca-9c47-99f624614658/message/msg-789-xyz", + type: "uk.nhs.notify.client-callbacks.message.status.transitioned.v1", + time: "2026-02-05T14:30:00.000Z", + datacontenttype: "application/json", + dataschema: "https://nhs.uk/schemas/notify/message-status-data.v1.json", + traceparent: "00-4d678967f96e353c07a0a31c1849b500-07f83ba58dd8df70-01", + data: { + clientId: "client-abc-123", + messageId: "msg-789-xyz", + messageReference: "client-ref-12345", + messageStatus: "DELIVERED", + messageStatusDescription: "Message successfully delivered", + channels: [ + { + type: "NHSAPP", + channelStatus: "DELIVERED", + }, + ], + timestamp: "2026-02-05T14:29:55Z", + routingPlan: { + id: "routing-plan-123", + name: "NHS App with SMS fallback", + version: "ztoe2qRAM8M8vS0bqajhyEBcvXacrGPp", + createdDate: "2023-11-17T14:27:51.413Z", }, - }); + }, + }; - const result = await handler(eventStr); - expect(result).toEqual({ - body: { - dataschemaversion: "1.0", - type: "uk.nhs.notify.client-callbacks.test-sid", + it("should transform a valid message status event from SQS", async () => { + const sqsMessage: SQSRecord = { + messageId: "sqs-msg-id-12345", + receiptHandle: "receipt-handle-xyz", + body: JSON.stringify(validMessageStatusEvent), + attributes: { + ApproximateReceiveCount: "1", + SentTimestamp: "1519211230", + SenderId: "ABCDEFGHIJ", + ApproximateFirstReceiveTimestamp: "1519211230", }, - }); + messageAttributes: {}, + md5OfBody: "mock-md5", + eventSource: "aws:sqs", + eventSourceARN: "arn:aws:sqs:eu-west-2:123456789:mock-queue", + awsRegion: "eu-west-2", + }; + + const result = await handler([sqsMessage]); + + expect(result).toHaveLength(1); + expect(result[0]).toHaveProperty("transformedPayload"); + const dataItem = result[0].transformedPayload.data[0]; + expect(dataItem.type).toBe("MessageStatus"); + expect((dataItem.attributes as MessageStatusAttributes).messageStatus).toBe( + "delivered", + ); }); - it("extracts from an array with nested body", async () => { - const eventArray = [ + it("should handle batch of SQS messages from EventBridge Pipes", async () => { + const sqsMessages: SQSRecord[] = [ { - messageId: "123", - body: JSON.stringify({ - body: { - dataschemaversion: "1.0", - type: "uk.nhs.notify.client-callbacks.test-sid", - }, - }), + messageId: "sqs-msg-id-1", + receiptHandle: "receipt-handle-1", + body: JSON.stringify(validMessageStatusEvent), + attributes: { + ApproximateReceiveCount: "1", + SentTimestamp: "1519211230", + SenderId: "ABCDEFGHIJ", + ApproximateFirstReceiveTimestamp: "1519211230", + }, + messageAttributes: {}, + md5OfBody: "mock-md5", + eventSource: "aws:sqs", + eventSourceARN: "arn:aws:sqs:eu-west-2:123456789:mock-queue", + awsRegion: "eu-west-2", + }, + { + messageId: "sqs-msg-id-2", + receiptHandle: "receipt-handle-2", + body: JSON.stringify(validMessageStatusEvent), + attributes: { + ApproximateReceiveCount: "1", + SentTimestamp: "1519211230", + SenderId: "ABCDEFGHIJ", + ApproximateFirstReceiveTimestamp: "1519211230", + }, + messageAttributes: {}, + md5OfBody: "mock-md5", + eventSource: "aws:sqs", + eventSourceARN: "arn:aws:sqs:eu-west-2:123456789:mock-queue", + awsRegion: "eu-west-2", }, ]; - const result = await handler(eventArray); - expect(result).toEqual({ - body: { - dataschemaversion: "1.0", - type: "uk.nhs.notify.client-callbacks.test-sid", + const result = await handler(sqsMessages); + + expect(result).toHaveLength(2); + expect(result[0]).toHaveProperty("transformedPayload"); + expect(result[1]).toHaveProperty("transformedPayload"); + }); + + it("should throw error for unsupported event type", async () => { + const unsupportedEvent = { + ...validMessageStatusEvent, + type: "uk.nhs.notify.client-callbacks.unsupported.v1", + }; + + const sqsMessage: SQSRecord = { + messageId: "sqs-msg-id-error", + receiptHandle: "receipt-handle-error", + body: JSON.stringify(unsupportedEvent), + attributes: { + ApproximateReceiveCount: "1", + SentTimestamp: "1519211230", + SenderId: "ABCDEFGHIJ", + ApproximateFirstReceiveTimestamp: "1519211230", }, - }); + messageAttributes: {}, + md5OfBody: "mock-md5", + eventSource: "aws:sqs", + eventSourceARN: "arn:aws:sqs:eu-west-2:123456789:mock-queue", + awsRegion: "eu-west-2", + }; + + await expect(handler([sqsMessage])).rejects.toThrow( + 'Validation failed: type: Invalid option: expected one of "uk.nhs.notify.client-callbacks.message.status.transitioned.v1"|"uk.nhs.notify.client-callbacks.channel.status.transitioned.v1"', + ); }); - it("returns empty body if fields are missing", async () => { - const event = { some: "random" }; - const result = await handler(event); - expect(result).toEqual({ body: {} }); + it("should transform a valid channel status event from SQS", async () => { + const validChannelStatusEvent: StatusTransitionEvent = { + specversion: "1.0", + id: "channel-event-123", + source: + "/nhs/england/notify/development/primary/data-plane/client-callbacks", + subject: + "customer/920fca11-596a-4eca-9c47-99f624614658/message/msg-456-abc/channel/nhsapp", + type: "uk.nhs.notify.client-callbacks.channel.status.transitioned.v1", + time: "2026-02-05T14:30:00.000Z", + datacontenttype: "application/json", + dataschema: "https://nhs.uk/schemas/notify/channel-status-data.v1.json", + traceparent: "00-4d678967f96e353c07a0a31c1849b500-07f83ba58dd8df70-02", + data: { + clientId: "client-abc-123", + messageId: "msg-789-xyz", + messageReference: "client-ref-12345", + channel: "NHSAPP", + channelStatus: "DELIVERED", + channelStatusDescription: "Successfully delivered to NHS App", + supplierStatus: "DELIVERED", + cascadeType: "primary", + cascadeOrder: 1, + timestamp: "2026-02-05T14:29:55Z", + retryCount: 0, + }, + }; + + const sqsMessage: SQSRecord = { + messageId: "sqs-channel-msg-id", + receiptHandle: "receipt-handle-channel", + body: JSON.stringify(validChannelStatusEvent), + attributes: { + ApproximateReceiveCount: "1", + SentTimestamp: "1519211230", + SenderId: "ABCDEFGHIJ", + ApproximateFirstReceiveTimestamp: "1519211230", + }, + messageAttributes: {}, + md5OfBody: "mock-md5", + eventSource: "aws:sqs", + eventSourceARN: "arn:aws:sqs:eu-west-2:123456789:mock-queue", + awsRegion: "eu-west-2", + }; + + const result = await handler([sqsMessage]); + + expect(result).toHaveLength(1); + expect(result[0]).toHaveProperty("transformedPayload"); + const dataItem = result[0].transformedPayload.data[0]; + expect(dataItem.type).toBe("ChannelStatus"); + expect((dataItem.attributes as ChannelStatusAttributes).channelStatus).toBe( + "delivered", + ); + expect((dataItem.attributes as ChannelStatusAttributes).channel).toBe( + "nhsapp", + ); }); - it("handles deeply nested fields", async () => { - const event = { - level1: { - level2: { - body: JSON.stringify({ - body: { - dataschemaversion: "2.0", - type: "nested-type", - }, - }), - }, + it("should throw error for invalid JSON in SQS message body", async () => { + const sqsMessage: SQSRecord = { + messageId: "sqs-msg-id-invalid", + receiptHandle: "receipt-handle-invalid", + body: "{ invalid json", + attributes: { + ApproximateReceiveCount: "1", + SentTimestamp: "1519211230", + SenderId: "ABCDEFGHIJ", + ApproximateFirstReceiveTimestamp: "1519211230", }, + messageAttributes: {}, + md5OfBody: "mock-md5", + eventSource: "aws:sqs", + eventSourceARN: "arn:aws:sqs:eu-west-2:123456789:mock-queue", + awsRegion: "eu-west-2", }; - const result = await handler(event); - expect(result).toEqual({ - body: { - dataschemaversion: "2.0", - type: "nested-type", + await expect(handler([sqsMessage])).rejects.toThrow( + "Failed to parse SQS message body as JSON", + ); + }); + + it("should handle validation errors and emit metrics", async () => { + const invalidEvent = { + ...validMessageStatusEvent, + data: { + ...validMessageStatusEvent.data, + clientId: "", }, - }); + }; + + const sqsMessage: SQSRecord = { + messageId: "sqs-msg-validation-error", + receiptHandle: "receipt-handle-validation", + body: JSON.stringify(invalidEvent), + attributes: { + ApproximateReceiveCount: "1", + SentTimestamp: "1519211230", + SenderId: "ABCDEFGHIJ", + ApproximateFirstReceiveTimestamp: "1519211230", + }, + messageAttributes: {}, + md5OfBody: "mock-md5", + eventSource: "aws:sqs", + eventSourceARN: "arn:aws:sqs:eu-west-2:123456789:mock-queue", + awsRegion: "eu-west-2", + }; + + await expect(handler([sqsMessage])).rejects.toThrow("Validation failed"); }); - it("handles invalid JSON gracefully", async () => { - const eventStr = "{ invalid json "; - const result = await handler(eventStr); - expect(result).toEqual({ body: {} }); + it("should process empty batch successfully", async () => { + const result = await handler([]); + + expect(result).toEqual([]); + }); + + it("should handle mixed message and channel status events in batch", async () => { + const channelStatusEvent: StatusTransitionEvent = { + specversion: "1.0", + id: "channel-event-456", + source: + "/nhs/england/notify/development/primary/data-plane/client-callbacks", + subject: + "customer/920fca11-596a-4eca-9c47-99f624614658/message/msg-456-abc/channel/sms", + type: "uk.nhs.notify.client-callbacks.channel.status.transitioned.v1", + time: "2026-02-05T14:30:00.000Z", + datacontenttype: "application/json", + dataschema: "https://nhs.uk/schemas/notify/channel-status-data.v1.json", + traceparent: "00-5e789078g07f464d08b1b42d2950c611-08g94cb69ee9eg81-02", + data: { + clientId: "client-xyz-789", + messageId: "msg-456-abc", + messageReference: "client-ref-67890", + channel: "SMS", + channelStatus: "FAILED", + channelStatusDescription: "SMS delivery failed", + channelFailureReasonCode: "SMS_001", + supplierStatus: "PERMANENT_FAILURE", + cascadeType: "secondary", + cascadeOrder: 2, + timestamp: "2026-02-05T14:30:00Z", + retryCount: 1, + }, + }; + + const sqsMessages: SQSRecord[] = [ + { + messageId: "sqs-msg-1", + receiptHandle: "receipt-1", + body: JSON.stringify(validMessageStatusEvent), + attributes: { + ApproximateReceiveCount: "1", + SentTimestamp: "1519211230", + SenderId: "ABCDEFGHIJ", + ApproximateFirstReceiveTimestamp: "1519211230", + }, + messageAttributes: {}, + md5OfBody: "mock-md5-1", + eventSource: "aws:sqs", + eventSourceARN: "arn:aws:sqs:eu-west-2:123456789:mock-queue", + awsRegion: "eu-west-2", + }, + { + messageId: "sqs-msg-2", + receiptHandle: "receipt-2", + body: JSON.stringify(channelStatusEvent), + attributes: { + ApproximateReceiveCount: "1", + SentTimestamp: "1519211231", + SenderId: "ABCDEFGHIJ", + ApproximateFirstReceiveTimestamp: "1519211231", + }, + messageAttributes: {}, + md5OfBody: "mock-md5-2", + eventSource: "aws:sqs", + eventSourceARN: "arn:aws:sqs:eu-west-2:123456789:mock-queue", + awsRegion: "eu-west-2", + }, + ]; + + const result = await handler(sqsMessages); + + expect(result).toHaveLength(2); + expect(result[0].transformedPayload.data[0].type).toBe("MessageStatus"); + expect(result[1].transformedPayload.data[0].type).toBe("ChannelStatus"); + }); +}); + +describe("createHandler default wiring", () => { + it("should construct default observability dependencies and delegate to processEvents", async () => { + jest.resetModules(); + + const state = { + createMetricLogger: jest.fn(), + CallbackMetrics: jest.fn(), + LoggerCtor: jest.fn(), + ObservabilityServiceCtor: jest.fn(), + processEvents: jest.fn(), + mockMetricsLogger: { + flush: jest.fn().mockImplementation(async () => {}), + }, + mockMetricsInstance: { emitEventReceived: jest.fn() }, + mockLoggerInstance: { info: jest.fn(), child: jest.fn() }, + mockObservabilityInstance: { + flush: jest.fn().mockImplementation(async () => {}), + }, + testHandler: undefined as + | ((event: SQSRecord[]) => Promise) + | undefined, + }; + + jest.isolateModules(() => { + state.createMetricLogger.mockReturnValue(state.mockMetricsLogger); + state.CallbackMetrics.mockReturnValue(state.mockMetricsInstance); + state.LoggerCtor.mockReturnValue(state.mockLoggerInstance); + state.ObservabilityServiceCtor.mockReturnValue( + state.mockObservabilityInstance, + ); + state.processEvents.mockResolvedValue(["ok"]); + + jest.doMock("services/metrics", () => ({ + createMetricLogger: state.createMetricLogger, + CallbackMetrics: state.CallbackMetrics, + })); + + jest.doMock("services/logger", () => ({ + Logger: state.LoggerCtor, + })); + + jest.doMock("services/observability", () => ({ + ObservabilityService: state.ObservabilityServiceCtor, + })); + + jest.doMock("handler", () => ({ + processEvents: state.processEvents, + })); + + const moduleUnderTest = jest.requireActual(".."); + state.testHandler = moduleUnderTest.createHandler(); + }); + + expect(state.testHandler).toBeDefined(); + const result = await state.testHandler!([]); + + expect(state.createMetricLogger).toHaveBeenCalledTimes(1); + expect(state.CallbackMetrics).toHaveBeenCalledWith(state.mockMetricsLogger); + expect(state.LoggerCtor).toHaveBeenCalledTimes(1); + expect(state.ObservabilityServiceCtor).toHaveBeenCalledWith( + state.mockLoggerInstance, + state.mockMetricsInstance, + state.mockMetricsLogger, + ); + expect(state.processEvents).toHaveBeenCalledWith( + [], + state.mockObservabilityInstance, + ); + expect(result).toEqual(["ok"]); + + jest.unmock("services/metrics"); + jest.unmock("services/logger"); + jest.unmock("services/observability"); + jest.unmock("handler"); }); }); diff --git a/lambdas/client-transform-filter-lambda/src/__tests__/services/callback-logger.test.ts b/lambdas/client-transform-filter-lambda/src/__tests__/services/callback-logger.test.ts new file mode 100644 index 0000000..71d43a6 --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/__tests__/services/callback-logger.test.ts @@ -0,0 +1,269 @@ +import { logCallbackGenerated } from "services/callback-logger"; +import type { Logger } from "services/logger"; +import type { ClientCallbackPayload } from "models/client-callback-payload"; +import { EventTypes } from "models/status-transition-event"; + +describe("callback-logger", () => { + let mockLogger: jest.Mocked; + + beforeEach(() => { + mockLogger = { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + child: jest.fn(), + addContext: jest.fn(), + clearContext: jest.fn(), + } as unknown as jest.Mocked; + }); + + describe("logCallbackGenerated", () => { + describe("MESSAGE_STATUS_TRANSITIONED events", () => { + const messageStatusPayload: ClientCallbackPayload = { + data: [ + { + type: "MessageStatus", + attributes: { + messageId: "msg-123", + messageReference: "ref-456", + messageStatus: "delivered", + messageStatusDescription: "Message successfully delivered", + messageFailureReasonCode: undefined, + channels: [ + { + type: "nhsapp", + channelStatus: "delivered", + }, + ], + timestamp: "2026-02-05T14:29:55Z", + routingPlan: { + id: "routing-plan-123", + name: "NHS App with SMS fallback", + version: "v1", + createdDate: "2023-11-17T14:27:51.413Z", + }, + }, + links: { + message: "/v1/message-batches/messages/msg-123", + }, + meta: { + idempotencyKey: "661f9510-f39c-52e5-b827-557766551111", + }, + }, + ], + }; + + it("should log message status callback with all fields", () => { + logCallbackGenerated( + mockLogger, + messageStatusPayload, + EventTypes.MESSAGE_STATUS_TRANSITIONED, + "corr-123", + "client-abc", + ); + + expect(mockLogger.info).toHaveBeenCalledWith("Callback generated", { + correlationId: "corr-123", + callbackType: "MessageStatus", + clientId: "client-abc", + messageId: "msg-123", + messageReference: "ref-456", + messageStatus: "delivered", + messageStatusDescription: "Message successfully delivered", + messageFailureReasonCode: undefined, + channels: [ + { + type: "nhsapp", + channelStatus: "delivered", + }, + ], + }); + }); + + it("should log message status callback with failure reason code", () => { + const failedPayload: ClientCallbackPayload = { + data: [ + { + ...messageStatusPayload.data[0], + attributes: { + ...messageStatusPayload.data[0].attributes, + messageStatus: "failed", + messageStatusDescription: "All channels failed", + messageFailureReasonCode: "ERR_INVALID_RECIPIENT", + }, + }, + ], + }; + + logCallbackGenerated( + mockLogger, + failedPayload, + EventTypes.MESSAGE_STATUS_TRANSITIONED, + "corr-456", + "client-xyz", + ); + + expect(mockLogger.info).toHaveBeenCalledWith( + "Callback generated", + expect.objectContaining({ + messageStatus: "failed", + messageFailureReasonCode: "ERR_INVALID_RECIPIENT", + }), + ); + }); + + it("should handle undefined correlationId", () => { + logCallbackGenerated( + mockLogger, + messageStatusPayload, + EventTypes.MESSAGE_STATUS_TRANSITIONED, + undefined, + "client-abc", + ); + + expect(mockLogger.info).toHaveBeenCalledWith( + "Callback generated", + expect.objectContaining({ + correlationId: undefined, + }), + ); + }); + }); + + describe("CHANNEL_STATUS_TRANSITIONED events", () => { + const channelStatusPayload: ClientCallbackPayload = { + data: [ + { + type: "ChannelStatus", + attributes: { + messageId: "msg-456", + messageReference: "ref-789", + cascadeType: "primary", + cascadeOrder: 1, + channel: "sms", + channelStatus: "delivered", + channelStatusDescription: "SMS delivered successfully", + channelFailureReasonCode: undefined, + supplierStatus: "delivered", + timestamp: "2026-02-05T14:30:00Z", + retryCount: 0, + }, + links: { + message: "/v1/message-batches/messages/msg-456", + }, + meta: { + idempotencyKey: "762f9510-f39c-52e5-b827-557766552222", + }, + }, + ], + }; + + it("should log channel status callback with all fields", () => { + logCallbackGenerated( + mockLogger, + channelStatusPayload, + EventTypes.CHANNEL_STATUS_TRANSITIONED, + "corr-789", + "client-def", + ); + + expect(mockLogger.info).toHaveBeenCalledWith("Callback generated", { + correlationId: "corr-789", + callbackType: "ChannelStatus", + clientId: "client-def", + messageId: "msg-456", + messageReference: "ref-789", + channel: "sms", + channelStatus: "delivered", + channelStatusDescription: "SMS delivered successfully", + channelFailureReasonCode: undefined, + supplierStatus: "delivered", + }); + }); + + it("should log channel status callback with failure reason code", () => { + const failedPayload: ClientCallbackPayload = { + data: [ + { + ...channelStatusPayload.data[0], + attributes: { + ...channelStatusPayload.data[0].attributes, + channelStatus: "failed", + channelStatusDescription: "Invalid phone number", + channelFailureReasonCode: "ERR_INVALID_PHONE_NUMBER", + supplierStatus: "permanent_failure", + }, + }, + ], + }; + + logCallbackGenerated( + mockLogger, + failedPayload, + EventTypes.CHANNEL_STATUS_TRANSITIONED, + "corr-999", + "client-ghi", + ); + + expect(mockLogger.info).toHaveBeenCalledWith( + "Callback generated", + expect.objectContaining({ + channelStatus: "failed", + channelFailureReasonCode: "ERR_INVALID_PHONE_NUMBER", + supplierStatus: "permanent_failure", + }), + ); + }); + }); + + describe("unsupported event types", () => { + const genericPayload: ClientCallbackPayload = { + data: [ + { + type: "MessageStatus", + attributes: { + messageId: "msg-123", + messageReference: "ref-456", + messageStatus: "delivered", + messageStatusDescription: "Message successfully delivered", + messageFailureReasonCode: undefined, + channels: [], + timestamp: "2026-02-05T14:29:55Z", + routingPlan: { + id: "routing-plan-123", + name: "Test", + version: "v1", + createdDate: "2023-11-17T14:27:51.413Z", + }, + }, + links: { + message: "/v1/message-batches/messages/msg-123", + }, + meta: { + idempotencyKey: "661f9510-f39c-52e5-b827-557766551111", + }, + }, + ], + }; + + it("should log with common fields only for unknown event type", () => { + logCallbackGenerated( + mockLogger, + genericPayload, + "uk.nhs.notify.unknown.event.type", + "corr-000", + "client-zzz", + ); + + expect(mockLogger.info).toHaveBeenCalledWith("Callback generated", { + correlationId: "corr-000", + callbackType: "MessageStatus", + clientId: "client-zzz", + messageId: "msg-123", + messageReference: "ref-456", + }); + }); + }); + }); +}); diff --git a/lambdas/client-transform-filter-lambda/src/__tests__/services/error-handler.test.ts b/lambdas/client-transform-filter-lambda/src/__tests__/services/error-handler.test.ts new file mode 100644 index 0000000..8ce1ff1 --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/__tests__/services/error-handler.test.ts @@ -0,0 +1,533 @@ +import { + ConfigLoadingError, + ErrorType, + LambdaError, + TransformationError, + ValidationError, + formatErrorForLogging, + getEventError, + isRetriable, + wrapUnknownError, +} from "services/error-handler"; + +describe("ErrorType", () => { + it("should define all error types", () => { + expect(ErrorType.VALIDATION_ERROR).toBe("ValidationError"); + expect(ErrorType.CONFIG_LOADING_ERROR).toBe("ConfigLoadingError"); + expect(ErrorType.TRANSFORMATION_ERROR).toBe("TransformationError"); + expect(ErrorType.UNKNOWN_ERROR).toBe("UnknownError"); + }); +}); + +describe("LambdaError", () => { + it("should create error with all properties", () => { + const error = new LambdaError( + ErrorType.UNKNOWN_ERROR, + "Test error", + "corr-123", + true, + ); + + expect(error.message).toBe("Test error"); + expect(error.errorType).toBe(ErrorType.UNKNOWN_ERROR); + expect(error.correlationId).toBe("corr-123"); + expect(error.retryable).toBe(true); + expect(error.name).toBe("LambdaError"); + expect(error).toBeInstanceOf(Error); + }); + + it("should create error with optional parameters", () => { + const error = new LambdaError(ErrorType.UNKNOWN_ERROR, "Test error"); + + expect(error.message).toBe("Test error"); + expect(error.errorType).toBe(ErrorType.UNKNOWN_ERROR); + expect(error.correlationId).toBeUndefined(); + expect(error.retryable).toBe(false); + }); + + it("should maintain stack trace", () => { + const error = new LambdaError(ErrorType.UNKNOWN_ERROR, "Test error"); + expect(error.stack).toBeDefined(); + expect(error.stack).toContain("LambdaError"); + }); + + it("should have correct properties", () => { + const error = new LambdaError( + ErrorType.VALIDATION_ERROR, + "Invalid schema", + "corr-789", + false, + ); + + expect(error.errorType).toBe(ErrorType.VALIDATION_ERROR); + expect(error.message).toBe("Invalid schema"); + expect(error.correlationId).toBe("corr-789"); + expect(error.retryable).toBe(false); + }); + + it("should have correct properties without optional fields", () => { + const error = new LambdaError(ErrorType.UNKNOWN_ERROR, "Test error"); + + expect(error.errorType).toBe(ErrorType.UNKNOWN_ERROR); + expect(error.message).toBe("Test error"); + expect(error.correlationId).toBeUndefined(); + expect(error.retryable).toBe(false); + }); +}); + +describe("ValidationError", () => { + it("should create non-retriable validation error", () => { + const error = new ValidationError("Schema mismatch", "corr-123"); + + expect(error.message).toBe("Schema mismatch"); + expect(error.errorType).toBe(ErrorType.VALIDATION_ERROR); + expect(error.correlationId).toBe("corr-123"); + expect(error.retryable).toBe(false); + expect(error.name).toBe("ValidationError"); + }); + + it("should create validation error without optional parameters", () => { + const error = new ValidationError("Schema mismatch"); + + expect(error.message).toBe("Schema mismatch"); + expect(error.errorType).toBe(ErrorType.VALIDATION_ERROR); + expect(error.correlationId).toBeUndefined(); + expect(error.retryable).toBe(false); + }); + + it("should be instance of LambdaError and Error", () => { + const error = new ValidationError("Test"); + expect(error).toBeInstanceOf(ValidationError); + expect(error).toBeInstanceOf(LambdaError); + expect(error).toBeInstanceOf(Error); + }); +}); + +describe("ConfigLoadingError", () => { + it("should create retriable config loading error", () => { + const error = new ConfigLoadingError("S3 unavailable", "corr-123"); + + expect(error.message).toBe("S3 unavailable"); + expect(error.errorType).toBe(ErrorType.CONFIG_LOADING_ERROR); + expect(error.correlationId).toBe("corr-123"); + expect(error.retryable).toBe(true); + expect(error.name).toBe("ConfigLoadingError"); + }); + + it("should create config loading error without optional parameters", () => { + const error = new ConfigLoadingError("S3 unavailable"); + + expect(error.message).toBe("S3 unavailable"); + expect(error.errorType).toBe(ErrorType.CONFIG_LOADING_ERROR); + expect(error.correlationId).toBeUndefined(); + expect(error.retryable).toBe(true); + }); + + it("should be instance of LambdaError and Error", () => { + const error = new ConfigLoadingError("Test"); + expect(error).toBeInstanceOf(ConfigLoadingError); + expect(error).toBeInstanceOf(LambdaError); + expect(error).toBeInstanceOf(Error); + }); +}); + +describe("TransformationError", () => { + it("should create non-retriable transformation error", () => { + const error = new TransformationError("Missing field", "corr-123"); + + expect(error.message).toBe("Missing field"); + expect(error.errorType).toBe(ErrorType.TRANSFORMATION_ERROR); + expect(error.correlationId).toBe("corr-123"); + expect(error.retryable).toBe(false); + expect(error.name).toBe("TransformationError"); + }); + + it("should create transformation error without optional parameters", () => { + const error = new TransformationError("Missing field"); + + expect(error.message).toBe("Missing field"); + expect(error.errorType).toBe(ErrorType.TRANSFORMATION_ERROR); + expect(error.correlationId).toBeUndefined(); + expect(error.retryable).toBe(false); + }); + + it("should be instance of LambdaError and Error", () => { + const error = new TransformationError("Test"); + expect(error).toBeInstanceOf(TransformationError); + expect(error).toBeInstanceOf(LambdaError); + expect(error).toBeInstanceOf(Error); + }); +}); + +describe("wrapUnknownError", () => { + it("should return LambdaError as-is", () => { + const originalError = new ValidationError("Original", "corr-123"); + const wrapped = wrapUnknownError(originalError, "corr-789"); + + expect(wrapped).toBe(originalError); + expect(wrapped.correlationId).toBe("corr-123"); + }); + + it("should wrap standard Error", () => { + const originalError = new Error("Standard error"); + const wrapped = wrapUnknownError(originalError, "corr-123"); + + expect(wrapped).toBeInstanceOf(LambdaError); + expect(wrapped.message).toBe("Standard error"); + expect(wrapped.errorType).toBe(ErrorType.UNKNOWN_ERROR); + expect(wrapped.correlationId).toBe("corr-123"); + expect(wrapped.retryable).toBe(false); + }); + + it("should wrap Error without optional parameters", () => { + const originalError = new Error("Standard error"); + const wrapped = wrapUnknownError(originalError); + + expect(wrapped).toBeInstanceOf(LambdaError); + expect(wrapped.message).toBe("Standard error"); + expect(wrapped.correlationId).toBeUndefined(); + }); + + it("should wrap string error", () => { + const wrapped = wrapUnknownError("String error", "corr-123"); + + expect(wrapped).toBeInstanceOf(LambdaError); + expect(wrapped.message).toBe("String error"); + expect(wrapped.errorType).toBe(ErrorType.UNKNOWN_ERROR); + expect(wrapped.correlationId).toBe("corr-123"); + expect(wrapped.retryable).toBe(false); + }); + + it("should wrap number error", () => { + const wrapped = wrapUnknownError(404, "corr-123"); + + expect(wrapped).toBeInstanceOf(LambdaError); + expect(wrapped.message).toBe("404"); + expect(wrapped.errorType).toBe(ErrorType.UNKNOWN_ERROR); + }); + + it("should wrap boolean error", () => { + const wrapped = wrapUnknownError(false, "corr-123"); + + expect(wrapped).toBeInstanceOf(LambdaError); + expect(wrapped.message).toBe("false"); + expect(wrapped.errorType).toBe(ErrorType.UNKNOWN_ERROR); + }); + + it("should wrap object error", () => { + const errorObj = { code: 500, details: "Internal error" }; + const wrapped = wrapUnknownError(errorObj, "corr-123"); + + expect(wrapped).toBeInstanceOf(LambdaError); + expect(wrapped.message).toBe(JSON.stringify(errorObj)); + expect(wrapped.errorType).toBe(ErrorType.UNKNOWN_ERROR); + }); + + it("should handle object with circular references", () => { + const circularObj: any = { name: "test" }; + circularObj.self = circularObj; + + const wrapped = wrapUnknownError(circularObj, "corr-123"); + + expect(wrapped).toBeInstanceOf(LambdaError); + expect(wrapped.message).toBe("Unknown error (unable to serialize)"); + expect(wrapped.errorType).toBe(ErrorType.UNKNOWN_ERROR); + }); + + it("should wrap null error", () => { + const wrapped = wrapUnknownError(null, "corr-123"); + + expect(wrapped).toBeInstanceOf(LambdaError); + expect(wrapped.message).toBe("Unknown error"); + expect(wrapped.errorType).toBe(ErrorType.UNKNOWN_ERROR); + }); + + it("should wrap undefined error", () => { + const wrapped = wrapUnknownError(undefined as unknown, "corr-123"); + + expect(wrapped).toBeInstanceOf(LambdaError); + expect(wrapped.message).toBe("Unknown error"); + expect(wrapped.errorType).toBe(ErrorType.UNKNOWN_ERROR); + }); + + it("should wrap array error", () => { + const wrapped = wrapUnknownError([1, 2, 3], "corr-123"); + + expect(wrapped).toBeInstanceOf(LambdaError); + expect(wrapped.message).toBe("[1,2,3]"); + expect(wrapped.errorType).toBe(ErrorType.UNKNOWN_ERROR); + }); +}); + +describe("isRetriable", () => { + it("should return true for retriable LambdaError", () => { + const error = new ConfigLoadingError("S3 error"); + expect(isRetriable(error)).toBe(true); + }); + + it("should return false for non-retriable ValidationError", () => { + const error = new ValidationError("Invalid schema"); + expect(isRetriable(error)).toBe(false); + }); + + it("should return false for non-retriable TransformationError", () => { + const error = new TransformationError("Missing field"); + expect(isRetriable(error)).toBe(false); + }); + + it("should return false for custom non-retriable LambdaError", () => { + const error = new LambdaError( + ErrorType.UNKNOWN_ERROR, + "Test", + undefined, + false, + ); + expect(isRetriable(error)).toBe(false); + }); + + it("should return true for custom retriable LambdaError", () => { + const error = new LambdaError( + ErrorType.UNKNOWN_ERROR, + "Test", + undefined, + true, + ); + expect(isRetriable(error)).toBe(true); + }); + + it("should return false for standard Error", () => { + const error = new Error("Standard error"); + expect(isRetriable(error)).toBe(false); + }); + + it("should return false for string error", () => { + expect(isRetriable("String error")).toBe(false); + }); + + it("should return false for null", () => { + expect(isRetriable(null)).toBe(false); + }); + + it("should return false for undefined", () => { + expect(isRetriable(undefined as unknown)).toBe(false); + }); + + it("should return false for number", () => { + expect(isRetriable(404)).toBe(false); + }); + + it("should return false for object", () => { + expect(isRetriable({ error: "test" })).toBe(false); + }); +}); + +describe("formatErrorForLogging", () => { + it("should format LambdaError with all fields", () => { + const error = new ValidationError("Invalid schema", "corr-123"); + const formatted = formatErrorForLogging(error); + + expect(formatted.errorType).toBe(ErrorType.VALIDATION_ERROR); + expect(formatted.message).toBe("Invalid schema"); + expect(formatted.retryable).toBe(false); + expect(formatted.stack).toBeDefined(); + expect(formatted.stack).toContain("ValidationError"); + }); + + it("should format retriable ConfigLoadingError", () => { + const error = new ConfigLoadingError("S3 unavailable"); + const formatted = formatErrorForLogging(error); + + expect(formatted.errorType).toBe(ErrorType.CONFIG_LOADING_ERROR); + expect(formatted.message).toBe("S3 unavailable"); + expect(formatted.retryable).toBe(true); + expect(formatted.stack).toBeDefined(); + }); + + it("should format TransformationError", () => { + const error = new TransformationError("Missing field"); + const formatted = formatErrorForLogging(error); + + expect(formatted.errorType).toBe(ErrorType.TRANSFORMATION_ERROR); + expect(formatted.message).toBe("Missing field"); + expect(formatted.retryable).toBe(false); + expect(formatted.stack).toBeDefined(); + }); + + it("should format standard Error", () => { + const error = new Error("Standard error"); + const formatted = formatErrorForLogging(error); + + expect(formatted.errorType).toBe(ErrorType.UNKNOWN_ERROR); + expect(formatted.message).toBe("Standard error"); + expect(formatted.retryable).toBe(false); + expect(formatted.stack).toBeDefined(); + expect(formatted.stack).toContain("Error"); + }); + + it("should format standard Error without stack", () => { + const error = new Error("Test error"); + delete error.stack; + const formatted = formatErrorForLogging(error); + + expect(formatted.errorType).toBe(ErrorType.UNKNOWN_ERROR); + expect(formatted.message).toBe("Test error"); + expect(formatted.retryable).toBe(false); + expect(formatted.stack).toBeUndefined(); + }); + + it("should format string error", () => { + const formatted = formatErrorForLogging("String error"); + + expect(formatted.errorType).toBe(ErrorType.UNKNOWN_ERROR); + expect(formatted.message).toBe("String error"); + expect(formatted.retryable).toBe(false); + expect(formatted.stack).toBeUndefined(); + }); + + it("should format number error", () => { + const formatted = formatErrorForLogging(404); + + expect(formatted.errorType).toBe(ErrorType.UNKNOWN_ERROR); + expect(formatted.message).toBe("404"); + expect(formatted.retryable).toBe(false); + expect(formatted.stack).toBeUndefined(); + }); + + it("should format boolean error", () => { + const formatted = formatErrorForLogging(false); + + expect(formatted.errorType).toBe(ErrorType.UNKNOWN_ERROR); + expect(formatted.message).toBe("false"); + expect(formatted.retryable).toBe(false); + expect(formatted.stack).toBeUndefined(); + }); + + it("should format object error", () => { + const errorObj = { code: 500, details: "Server error" }; + const formatted = formatErrorForLogging(errorObj); + + expect(formatted.errorType).toBe(ErrorType.UNKNOWN_ERROR); + expect(formatted.message).toBe(JSON.stringify(errorObj)); + expect(formatted.retryable).toBe(false); + expect(formatted.stack).toBeUndefined(); + }); + + it("should format null error", () => { + const formatted = formatErrorForLogging(null); + + expect(formatted.errorType).toBe(ErrorType.UNKNOWN_ERROR); + expect(formatted.message).toBe("Unknown error"); + expect(formatted.retryable).toBe(false); + expect(formatted.stack).toBeUndefined(); + }); + + it("should format undefined error", () => { + const formatted = formatErrorForLogging(undefined as unknown); + + expect(formatted.errorType).toBe(ErrorType.UNKNOWN_ERROR); + expect(formatted.message).toBe("Unknown error"); + expect(formatted.retryable).toBe(false); + expect(formatted.stack).toBeUndefined(); + }); + + it("should format object with circular reference", () => { + const circularObj: any = { name: "test" }; + circularObj.self = circularObj; + + const formatted = formatErrorForLogging(circularObj); + + expect(formatted.errorType).toBe(ErrorType.UNKNOWN_ERROR); + expect(formatted.message).toBe("Unknown error (unable to serialize)"); + expect(formatted.retryable).toBe(false); + expect(formatted.stack).toBeUndefined(); + }); +}); + +describe("getEventError", () => { + const mockMetrics = { + emitValidationError: jest.fn(), + emitTransformationFailure: jest.fn(), + }; + + const mockEventLogger = { + error: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should return ValidationError and emit validation metric", () => { + const error = new ValidationError("Invalid event", "corr-validation"); + + const result = getEventError( + error, + mockMetrics, + mockEventLogger, + "message.status.transitioned", + ); + + expect(result).toBe(error); + expect(mockEventLogger.error).toHaveBeenCalledWith( + "Event validation failed", + { + correlationId: "corr-validation", + error, + }, + ); + expect(mockMetrics.emitValidationError).toHaveBeenCalled(); + expect(mockMetrics.emitTransformationFailure).not.toHaveBeenCalled(); + }); + + it("should return TransformationError and emit transformation metric", () => { + const error = new TransformationError( + "Transformation failed", + "corr-transform", + ); + + const result = getEventError( + error, + mockMetrics, + mockEventLogger, + "channel.status.transitioned", + ); + + expect(result).toBe(error); + expect(mockEventLogger.error).toHaveBeenCalledWith( + "Event transformation failed", + { + correlationId: "corr-transform", + eventType: "channel.status.transitioned", + error, + }, + ); + expect(mockMetrics.emitTransformationFailure).toHaveBeenCalled(); + expect(mockMetrics.emitValidationError).not.toHaveBeenCalled(); + }); + + it("should wrap unknown error and emit unknown transformation metric", () => { + const error = new Error("Unexpected runtime error"); + + const result = getEventError( + error, + mockMetrics, + mockEventLogger, + "message.status.transitioned", + ); + + expect(result).toBeInstanceOf(LambdaError); + expect(result.message).toBe("Unexpected runtime error"); + expect((result as LambdaError).errorType).toBe(ErrorType.UNKNOWN_ERROR); + expect((result as LambdaError).correlationId).toBe("unknown"); + + expect(mockEventLogger.error).toHaveBeenCalledWith( + "Unexpected error processing event", + { + correlationId: "unknown", + error: result, + }, + ); + expect(mockMetrics.emitTransformationFailure).toHaveBeenCalled(); + expect(mockMetrics.emitValidationError).not.toHaveBeenCalled(); + }); +}); diff --git a/lambdas/client-transform-filter-lambda/src/__tests__/services/logger.test.ts b/lambdas/client-transform-filter-lambda/src/__tests__/services/logger.test.ts new file mode 100644 index 0000000..5f6a09f --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/__tests__/services/logger.test.ts @@ -0,0 +1,369 @@ +import pino from "pino"; +import { + LogContext, + Logger, + extractCorrelationId, + logLifecycleEvent, + logger, +} from "services/logger"; + +jest.mock("pino", () => { + const mockLoggerMethods = { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + child: jest.fn(), + }; + return jest.fn(() => mockLoggerMethods); +}); + +const mockLoggerMethods = pino() as any; + +describe("Logger", () => { + beforeEach(() => { + jest.clearAllMocks(); + mockLoggerMethods.child.mockReturnValue(mockLoggerMethods); + }); + + describe("constructor", () => { + it("should create logger without initial context", () => { + const testLogger = new Logger(); + expect(testLogger).toBeInstanceOf(Logger); + }); + + it("should create logger with initial context", () => { + const initialContext: LogContext = { + correlationId: "test-corr-123", + clientId: "client-456", + }; + + const testLogger = new Logger(initialContext); + + expect(testLogger).toBeInstanceOf(Logger); + expect(mockLoggerMethods.child).toHaveBeenCalledWith(initialContext); + }); + }); + + describe("addContext", () => { + it("should add new context to logger", () => { + const testLogger = new Logger(); + const newContext: LogContext = { + correlationId: "corr-789", + }; + + testLogger.addContext(newContext); + + expect(mockLoggerMethods.child).toHaveBeenCalledWith(newContext); + }); + + it("should merge new context with existing context", () => { + const initialContext: LogContext = { + correlationId: "corr-123", + clientId: "client-456", + }; + const testLogger = new Logger(initialContext); + + mockLoggerMethods.child.mockClear(); + + const additionalContext: LogContext = { + messageId: "msg-101", + }; + + testLogger.addContext(additionalContext); + + expect(mockLoggerMethods.child).toHaveBeenCalledWith({ + correlationId: "corr-123", + clientId: "client-456", + + messageId: "msg-101", + }); + }); + + it("should override existing context keys", () => { + const initialContext: LogContext = { + correlationId: "old-corr", + clientId: "client-123", + }; + const testLogger = new Logger(initialContext); + + mockLoggerMethods.child.mockClear(); + + const newContext: LogContext = { + correlationId: "new-corr", + }; + + testLogger.addContext(newContext); + + expect(mockLoggerMethods.child).toHaveBeenCalledWith({ + correlationId: "new-corr", + clientId: "client-123", + }); + }); + }); + + describe("clearContext", () => { + it("should clear all context from logger", () => { + const initialContext: LogContext = { + correlationId: "corr-123", + clientId: "client-456", + }; + const testLogger = new Logger(initialContext); + + testLogger.clearContext(); + + expect(testLogger).toBeInstanceOf(Logger); + }); + }); + + describe("child", () => { + it("should create a child logger with new context", () => { + const testLogger = new Logger(); + const childContext: LogContext = { + correlationId: "corr-123", + }; + + const childLogger = testLogger.child(childContext); + + expect(childLogger).toBeInstanceOf(Logger); + expect(mockLoggerMethods.child).toHaveBeenCalledWith(childContext); + }); + + it("should merge parent context with child context", () => { + const parentContext: LogContext = { + correlationId: "parent-corr", + clientId: "client-123", + }; + const testLogger = new Logger(parentContext); + + mockLoggerMethods.child.mockClear(); + + const childContext: LogContext = { + messageId: "msg-101", + }; + + const childLogger = testLogger.child(childContext); + + expect(childLogger).toBeInstanceOf(Logger); + expect(mockLoggerMethods.child).toHaveBeenCalledWith({ + correlationId: "parent-corr", + clientId: "client-123", + + messageId: "msg-101", + }); + }); + }); + + describe("info", () => { + it("should log info message without additional context", () => { + const testLogger = new Logger(); + testLogger.info("Test info message"); + + expect(mockLoggerMethods.info).toHaveBeenCalledWith( + {}, + "Test info message", + ); + }); + + it("should log info message with additional context", () => { + const testLogger = new Logger(); + const context: LogContext = { + correlationId: "corr-123", + eventType: "status-update", + }; + + testLogger.info("Test info message", context); + + expect(mockLoggerMethods.info).toHaveBeenCalledWith( + context, + "Test info message", + ); + }); + }); + + describe("warn", () => { + it("should log warning message without additional context", () => { + const testLogger = new Logger(); + testLogger.warn("Test warning"); + + expect(mockLoggerMethods.warn).toHaveBeenCalledWith({}, "Test warning"); + }); + + it("should log warning message with additional context", () => { + const testLogger = new Logger(); + const context: LogContext = { + correlationId: "corr-456", + statusCode: 429, + }; + + testLogger.warn("Rate limit warning", context); + + expect(mockLoggerMethods.warn).toHaveBeenCalledWith( + context, + "Rate limit warning", + ); + }); + }); + + describe("error", () => { + it("should log error message without additional context", () => { + const testLogger = new Logger(); + testLogger.error("Test error"); + + expect(mockLoggerMethods.error).toHaveBeenCalledWith({}, "Test error"); + }); + + it("should log error message with additional context", () => { + const testLogger = new Logger(); + const error = new Error("Something failed"); + const context: LogContext = { + correlationId: "corr-789", + error, + }; + + testLogger.error("Operation failed", context); + + expect(mockLoggerMethods.error).toHaveBeenCalledWith( + context, + "Operation failed", + ); + }); + }); + + describe("debug", () => { + it("should log debug message without additional context", () => { + const testLogger = new Logger(); + testLogger.debug("Test debug"); + + expect(mockLoggerMethods.debug).toHaveBeenCalledWith({}, "Test debug"); + }); + + it("should log debug message with additional context", () => { + const testLogger = new Logger(); + const context: LogContext = { + correlationId: "corr-101", + }; + + testLogger.debug("Debug info", context); + + expect(mockLoggerMethods.debug).toHaveBeenCalledWith( + context, + "Debug info", + ); + }); + }); + + describe("singleton logger instance", () => { + it("should export a singleton logger instance", () => { + expect(logger).toBeInstanceOf(Logger); + }); + }); +}); + +describe("extractCorrelationId", () => { + it("should extract correlation ID from event.id", () => { + const event = { + id: "test-corr-123", + type: "status-update", + }; + + const correlationId = extractCorrelationId(event); + + expect(correlationId).toBe("test-corr-123"); + }); + + it("should return undefined when id is not present", () => { + const event = { + type: "status-update", + }; + + const correlationId = extractCorrelationId(event); + + expect(correlationId).toBeUndefined(); + }); + + it("should return undefined for null event", () => { + const correlationId = extractCorrelationId(null); + + expect(correlationId).toBeUndefined(); + }); + + it("should return undefined for undefined event", () => { + const correlationId = extractCorrelationId(undefined as unknown); + + expect(correlationId).toBeUndefined(); + }); + + it("should return undefined for empty object", () => { + const correlationId = extractCorrelationId({}); + + expect(correlationId).toBeUndefined(); + }); +}); + +describe("logLifecycleEvent", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should log processing-started lifecycle event", () => { + const testLogger = new Logger(); + const context: LogContext = { + correlationId: "corr-123", + }; + + logLifecycleEvent(testLogger, "processing-started", context); + + expect(mockLoggerMethods.info).toHaveBeenCalledWith( + context, + "Callback lifecycle: processing-started", + ); + }); + + it("should log transformation-completed lifecycle event", () => { + const testLogger = new Logger(); + const context: LogContext = { + correlationId: "corr-123", + messageId: "msg-789", + }; + + logLifecycleEvent(testLogger, "transformation-completed", context); + + expect(mockLoggerMethods.info).toHaveBeenCalledWith( + context, + "Callback lifecycle: transformation-completed", + ); + }); + + it("should log transformation-started lifecycle event", () => { + const testLogger = new Logger(); + const context: LogContext = { + correlationId: "corr-123", + eventType: "message.status.transitioned", + clientId: "client-456", + messageId: "msg-789", + }; + + logLifecycleEvent(testLogger, "transformation-started", context); + + expect(mockLoggerMethods.info).toHaveBeenCalledWith( + context, + "Callback lifecycle: transformation-started", + ); + }); + + it("should log delivery-initiated lifecycle event", () => { + const testLogger = new Logger(); + const context: LogContext = { + correlationId: "corr-123", + clientId: "client-456", + }; + + logLifecycleEvent(testLogger, "delivery-initiated", context); + + expect(mockLoggerMethods.info).toHaveBeenCalledWith( + context, + "Callback lifecycle: delivery-initiated", + ); + }); +}); diff --git a/lambdas/client-transform-filter-lambda/src/__tests__/services/metrics.test.ts b/lambdas/client-transform-filter-lambda/src/__tests__/services/metrics.test.ts new file mode 100644 index 0000000..b0e4578 --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/__tests__/services/metrics.test.ts @@ -0,0 +1,134 @@ +import { Unit, createMetricsLogger } from "aws-embedded-metrics"; +import { CallbackMetrics, createMetricLogger } from "services/metrics"; + +jest.mock("aws-embedded-metrics"); + +const mockPutMetric = jest.fn(); +const mockSetDimensions = jest.fn(); +const mockSetNamespace = jest.fn(); +const mockFlush = jest.fn(); + +const mockMetricsLogger = { + putMetric: mockPutMetric, + setDimensions: mockSetDimensions, + setNamespace: mockSetNamespace, + flush: mockFlush, +}; + +(createMetricsLogger as jest.Mock).mockReturnValue(mockMetricsLogger); + +describe("createMetricsLogger", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + delete process.env.METRICS_NAMESPACE; + delete process.env.ENVIRONMENT; + }); + + it("should throw if METRICS_NAMESPACE is not set", () => { + process.env.ENVIRONMENT = "production"; + + expect(() => createMetricLogger()).toThrow( + "METRICS_NAMESPACE environment variable is not set", + ); + }); + + it("should throw if ENVIRONMENT is not set", () => { + process.env.METRICS_NAMESPACE = "nhs-notify-client-callbacks-metrics"; + + expect(() => createMetricLogger()).toThrow( + "ENVIRONMENT environment variable is not set", + ); + }); + + it("should use METRICS_NAMESPACE environment variable", () => { + process.env.METRICS_NAMESPACE = "CustomNamespace"; + process.env.ENVIRONMENT = "production"; + + createMetricLogger(); + + expect(mockSetNamespace).toHaveBeenCalledWith("CustomNamespace"); + }); + + it("should use ENVIRONMENT environment variable", () => { + process.env.METRICS_NAMESPACE = "nhs-notify-client-callbacks-metrics"; + process.env.ENVIRONMENT = "production"; + + createMetricLogger(); + + expect(mockSetDimensions).toHaveBeenCalledWith({ + Environment: "production", + }); + }); +}); + +describe("CallbackMetrics", () => { + let callbackMetrics: CallbackMetrics; + + beforeEach(() => { + jest.clearAllMocks(); + callbackMetrics = new CallbackMetrics(mockMetricsLogger as any); + }); + + describe("emitEventReceived", () => { + it("should emit EventsReceived metric", () => { + callbackMetrics.emitEventReceived(); + + expect(mockPutMetric).toHaveBeenCalledWith( + "EventsReceived", + 1, + Unit.Count, + ); + }); + }); + + describe("emitTransformationSuccess", () => { + it("should emit TransformationsSuccessful metric", () => { + callbackMetrics.emitTransformationSuccess(); + + expect(mockPutMetric).toHaveBeenCalledWith( + "TransformationsSuccessful", + 1, + Unit.Count, + ); + }); + }); + + describe("emitTransformationFailure", () => { + it("should emit TransformationsFailed metric", () => { + callbackMetrics.emitTransformationFailure(); + + expect(mockPutMetric).toHaveBeenCalledWith( + "TransformationsFailed", + 1, + Unit.Count, + ); + }); + }); + + describe("emitDeliveryInitiated", () => { + it("should emit CallbacksInitiated metric", () => { + callbackMetrics.emitDeliveryInitiated(); + + expect(mockPutMetric).toHaveBeenCalledWith( + "CallbacksInitiated", + 1, + Unit.Count, + ); + }); + }); + + describe("emitValidationError", () => { + it("should emit ValidationErrors metric", () => { + callbackMetrics.emitValidationError(); + + expect(mockPutMetric).toHaveBeenCalledWith( + "ValidationErrors", + 1, + Unit.Count, + ); + }); + }); +}); diff --git a/lambdas/client-transform-filter-lambda/src/__tests__/transformers/channel-status-transformer.test.ts b/lambdas/client-transform-filter-lambda/src/__tests__/transformers/channel-status-transformer.test.ts new file mode 100644 index 0000000..4ca8830 --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/__tests__/transformers/channel-status-transformer.test.ts @@ -0,0 +1,272 @@ +import { transformChannelStatus } from "services/transformers/channel-status-transformer"; +import type { StatusTransitionEvent } from "models/status-transition-event"; +import type { ChannelStatusData } from "models/channel-status-data"; +import type { + ChannelStatusAttributes, + ClientCallbackPayload, +} from "models/client-callback-payload"; +import type { ChannelStatus, SupplierStatus } from "models/status-types"; +import type { Channel } from "models/channel-types"; + +describe("channel-status-transformer", () => { + describe("transformChannelStatus", () => { + const channelStatusEvent: StatusTransitionEvent = { + specversion: "1.0", + id: "SOME-GUID-a123-556677889999", + source: + "/nhs/england/notify/development/primary/data-plane/client-callbacks", + subject: + "customer/920fca11-596a-4eca-9c47-99f624614658/message/msg-456-abc/channel/nhsapp", + type: "uk.nhs.notify.client-callbacks.channel.status.transitioned.v1", + time: "2026-02-05T14:30:00.000Z", + datacontenttype: "application/json", + dataschema: "https://nhs.uk/schemas/notify/channel-status-data.v1.json", + traceparent: "00-4d678967f96e353c07a0a31c1849b500-07f83ba58dd8df70-02", + data: { + clientId: "client-abc-123", + messageId: "msg-789-xyz", + messageReference: "client-ref-12345", + channel: "NHSAPP", + channelStatus: "DELIVERED", + channelStatusDescription: "Successfully delivered to NHS App", + supplierStatus: "DELIVERED", + cascadeType: "primary", + cascadeOrder: 1, + timestamp: "2026-02-05T14:29:55Z", + retryCount: 0, + }, + }; + + it("should transform channel status event to JSON:API callback payload", () => { + const result: ClientCallbackPayload = + transformChannelStatus(channelStatusEvent); + + expect(result).toEqual({ + data: [ + { + type: "ChannelStatus", + attributes: { + messageId: "msg-789-xyz", + messageReference: "client-ref-12345", + channel: "nhsapp", + channelStatus: "delivered", + channelStatusDescription: "Successfully delivered to NHS App", + supplierStatus: "delivered", + cascadeType: "primary", + cascadeOrder: 1, + timestamp: "2026-02-05T14:29:55Z", + retryCount: 0, + }, + links: { + message: "/v1/message-batches/messages/msg-789-xyz", + }, + meta: { + idempotencyKey: "SOME-GUID-a123-556677889999", + }, + }, + ], + }); + }); + + it("should extract messageId from notify-data", () => { + const result = transformChannelStatus(channelStatusEvent); + const attrs = result.data[0].attributes as ChannelStatusAttributes; + + expect(attrs.messageId).toBe("msg-789-xyz"); + expect(attrs.messageReference).toBe("client-ref-12345"); + }); + + it("should extract channel from notify-data", () => { + const result = transformChannelStatus(channelStatusEvent); + const attrs = result.data[0].attributes as ChannelStatusAttributes; + + expect(attrs.channel).toBe("nhsapp"); + }); + + it("should extract channelStatus from notify-data", () => { + const result = transformChannelStatus(channelStatusEvent); + const attrs = result.data[0].attributes as ChannelStatusAttributes; + + expect(attrs.channelStatus).toBe("delivered"); + }); + + it("should extract supplierStatus from notify-data", () => { + const result = transformChannelStatus(channelStatusEvent); + const attrs = result.data[0].attributes as ChannelStatusAttributes; + + expect(attrs.supplierStatus).toBe("delivered"); + }); + + it("should extract cascadeType from notify-data", () => { + const result = transformChannelStatus(channelStatusEvent); + const attrs = result.data[0].attributes as ChannelStatusAttributes; + + expect(attrs.cascadeType).toBe("primary"); + }); + + it("should extract cascadeOrder from notify-data", () => { + const result = transformChannelStatus(channelStatusEvent); + const attrs = result.data[0].attributes as ChannelStatusAttributes; + + expect(attrs.cascadeOrder).toBe(1); + }); + + it("should extract timestamp from notify-data", () => { + const result = transformChannelStatus(channelStatusEvent); + const attrs = result.data[0].attributes as ChannelStatusAttributes; + + expect(attrs.timestamp).toBe("2026-02-05T14:29:55Z"); + }); + + it("should extract retryCount from notify-data", () => { + const result = transformChannelStatus(channelStatusEvent); + const attrs = result.data[0].attributes as ChannelStatusAttributes; + + expect(attrs.retryCount).toBe(0); + }); + + it("should include channelStatusDescription if present", () => { + const result = transformChannelStatus(channelStatusEvent); + const attrs = result.data[0].attributes as ChannelStatusAttributes; + + expect(attrs.channelStatusDescription).toBe( + "Successfully delivered to NHS App", + ); + }); + + it("should exclude channelStatusDescription if not present", () => { + const eventWithoutDescription = { + ...channelStatusEvent, + data: { + ...channelStatusEvent.data, + channelStatusDescription: undefined, + }, + }; + + const result = transformChannelStatus(eventWithoutDescription); + const attrs = result.data[0].attributes as ChannelStatusAttributes; + + expect(attrs.channelStatusDescription).toBeUndefined(); + }); + + it("should include channelFailureReasonCode if present", () => { + const eventWithFailure = { + ...channelStatusEvent, + data: { + ...channelStatusEvent.data, + channelStatus: "FAILED" as ChannelStatus, + channelFailureReasonCode: "RECIPIENT_INVALID", + }, + }; + + const result = transformChannelStatus(eventWithFailure); + const attrs = result.data[0].attributes as ChannelStatusAttributes; + + expect(attrs.channelFailureReasonCode).toBe("RECIPIENT_INVALID"); + }); + + it("should exclude channelFailureReasonCode if not present", () => { + const result = transformChannelStatus(channelStatusEvent); + const attrs = result.data[0].attributes as ChannelStatusAttributes; + + expect(attrs.channelFailureReasonCode).toBeUndefined(); + }); + + it("should handle previousChannelStatus for transition tracking", () => { + const eventWithPrevious = { + ...channelStatusEvent, + data: { + ...channelStatusEvent.data, + previousChannelStatus: "SENDING" as ChannelStatus, + }, + }; + + const result = transformChannelStatus(eventWithPrevious); + + // previousChannelStatus should be excluded from callback payload (operational field) + expect( + (result.data[0].attributes as any).previousChannelStatus, + ).toBeUndefined(); + }); + + it("should handle previousSupplierStatus for transition tracking", () => { + const eventWithPrevious = { + ...channelStatusEvent, + data: { + ...channelStatusEvent.data, + previousSupplierStatus: "RECEIVED" as SupplierStatus, + }, + }; + + const result = transformChannelStatus(eventWithPrevious); + + // previousSupplierStatus should be excluded from callback payload (operational field) + expect( + (result.data[0].attributes as any).previousSupplierStatus, + ).toBeUndefined(); + }); + + it("should construct message link using messageId", () => { + const result = transformChannelStatus(channelStatusEvent); + + expect(result.data[0].links.message).toBe( + "/v1/message-batches/messages/msg-789-xyz", + ); + }); + + it("should include idempotencyKey from event id in meta", () => { + const result = transformChannelStatus(channelStatusEvent); + + expect(result.data[0].meta.idempotencyKey).toBe( + "SOME-GUID-a123-556677889999", + ); + }); + + it("should exclude operational fields (clientId) from callback payload", () => { + const result = transformChannelStatus(channelStatusEvent); + + // Verify that clientId is not in the payload + expect((result.data[0].attributes as any).clientId).toBeUndefined(); + }); + + it("should set type as 'ChannelStatus' in data array", () => { + const result = transformChannelStatus(channelStatusEvent); + + expect(result.data[0].type).toBe("ChannelStatus"); + }); + + it("should handle retryCount > 0", () => { + const eventWithRetries = { + ...channelStatusEvent, + data: { + ...channelStatusEvent.data, + retryCount: 3, + }, + }; + + const result = transformChannelStatus(eventWithRetries); + const attrs = result.data[0].attributes as ChannelStatusAttributes; + + expect(attrs.retryCount).toBe(3); + }); + + it("should handle cascadeOrder for fallback channels", () => { + const fallbackEvent = { + ...channelStatusEvent, + data: { + ...channelStatusEvent.data, + channel: "SMS" as Channel, + cascadeType: "secondary" as "primary" | "secondary", + cascadeOrder: 2, + }, + }; + + const result = transformChannelStatus(fallbackEvent); + const attrs = result.data[0].attributes as ChannelStatusAttributes; + + expect(attrs.channel).toBe("sms"); + expect(attrs.cascadeType).toBe("secondary"); + expect(attrs.cascadeOrder).toBe(2); + }); + }); +}); diff --git a/lambdas/client-transform-filter-lambda/src/__tests__/transformers/message-status-transformer.test.ts b/lambdas/client-transform-filter-lambda/src/__tests__/transformers/message-status-transformer.test.ts new file mode 100644 index 0000000..4d97b15 --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/__tests__/transformers/message-status-transformer.test.ts @@ -0,0 +1,228 @@ +import { transformMessageStatus } from "services/transformers/message-status-transformer"; +import type { StatusTransitionEvent } from "models/status-transition-event"; +import type { MessageStatusData } from "models/message-status-data"; +import type { + ClientCallbackPayload, + MessageStatusAttributes, +} from "models/client-callback-payload"; +import type { MessageStatus } from "models/status-types"; + +describe("message-status-transformer", () => { + describe("transformMessageStatus", () => { + const messageStatusEvent: StatusTransitionEvent = { + specversion: "1.0", + id: "661f9510-f39c-52e5-b827-557766551111", + source: + "/nhs/england/notify/development/primary/data-plane/client-callbacks", + subject: + "customer/920fca11-596a-4eca-9c47-99f624614658/message/msg-789-xyz", + type: "uk.nhs.notify.client-callbacks.message.status.transitioned.v1", + time: "2026-02-05T14:30:00.000Z", + datacontenttype: "application/json", + dataschema: "https://nhs.uk/schemas/notify/message-status-data.v1.json", + traceparent: "00-4d678967f96e353c07a0a31c1849b500-07f83ba58dd8df70-01", + data: { + clientId: "client-abc-123", + messageId: "msg-789-xyz", + messageReference: "client-ref-12345", + messageStatus: "DELIVERED", + messageStatusDescription: "Message successfully delivered", + channels: [ + { + type: "NHSAPP", + channelStatus: "DELIVERED", + }, + { + type: "SMS", + channelStatus: "SKIPPED", + }, + ], + timestamp: "2026-02-05T14:29:55Z", + routingPlan: { + id: "routing-plan-123", + name: "NHS App with SMS fallback", + version: "ztoe2qRAM8M8vS0bqajhyEBcvXacrGPp", + createdDate: "2023-11-17T14:27:51.413Z", + }, + }, + }; + + it("should transform message status event to JSON:API callback payload", () => { + const result: ClientCallbackPayload = + transformMessageStatus(messageStatusEvent); + + expect(result).toEqual({ + data: [ + { + type: "MessageStatus", + attributes: { + messageId: "msg-789-xyz", + messageReference: "client-ref-12345", + messageStatus: "delivered", + messageStatusDescription: "Message successfully delivered", + channels: [ + { + type: "nhsapp", + channelStatus: "delivered", + }, + { + type: "sms", + channelStatus: "skipped", + }, + ], + timestamp: "2026-02-05T14:29:55Z", + routingPlan: { + id: "routing-plan-123", + name: "NHS App with SMS fallback", + version: "ztoe2qRAM8M8vS0bqajhyEBcvXacrGPp", + createdDate: "2023-11-17T14:27:51.413Z", + }, + }, + links: { + message: "/v1/message-batches/messages/msg-789-xyz", + }, + meta: { + idempotencyKey: "661f9510-f39c-52e5-b827-557766551111", + }, + }, + ], + }); + }); + + it("should extract messageId from notify-data", () => { + const result = transformMessageStatus(messageStatusEvent); + const attrs = result.data[0].attributes as MessageStatusAttributes; + + expect(attrs.messageId).toBe("msg-789-xyz"); + expect(attrs.messageReference).toBe("client-ref-12345"); + }); + + it("should extract messageStatus from notify-data", () => { + const result = transformMessageStatus(messageStatusEvent); + const attrs = result.data[0].attributes as MessageStatusAttributes; + + expect(attrs.messageStatus).toBe("delivered"); + }); + + it("should extract channels array from notify-data", () => { + const result = transformMessageStatus(messageStatusEvent); + const attrs = result.data[0].attributes as MessageStatusAttributes; + + expect(attrs.channels).toHaveLength(2); + expect(attrs.channels[0]).toEqual({ + type: "nhsapp", + channelStatus: "delivered", + }); + expect(attrs.channels[1]).toEqual({ + type: "sms", + channelStatus: "skipped", + }); + }); + + it("should extract timestamp from notify-data", () => { + const result = transformMessageStatus(messageStatusEvent); + const attrs = result.data[0].attributes as MessageStatusAttributes; + + expect(attrs.timestamp).toBe("2026-02-05T14:29:55Z"); + }); + + it("should construct routingPlan object from notify-data", () => { + const result = transformMessageStatus(messageStatusEvent); + const attrs = result.data[0].attributes as MessageStatusAttributes; + + expect(attrs.routingPlan).toEqual({ + id: "routing-plan-123", + name: "NHS App with SMS fallback", + version: "ztoe2qRAM8M8vS0bqajhyEBcvXacrGPp", + createdDate: "2023-11-17T14:27:51.413Z", + }); + }); + + it("should include messageStatusDescription if present", () => { + const result = transformMessageStatus(messageStatusEvent); + const attrs = result.data[0].attributes as MessageStatusAttributes; + + expect(attrs.messageStatusDescription).toBe( + "Message successfully delivered", + ); + }); + + it("should exclude messageStatusDescription if not present", () => { + const eventWithoutDescription = { + ...messageStatusEvent, + data: { + ...messageStatusEvent.data, + messageStatusDescription: undefined, + }, + }; + + const result = transformMessageStatus(eventWithoutDescription); + const attrs = result.data[0].attributes as MessageStatusAttributes; + + expect(attrs.messageStatusDescription).toBeUndefined(); + }); + + it("should include messageFailureReasonCode if present", () => { + const eventWithFailure = { + ...messageStatusEvent, + data: { + ...messageStatusEvent.data, + messageStatus: "FAILED" as MessageStatus, + messageFailureReasonCode: "DELIVERY_TIMEOUT", + }, + }; + + const result = transformMessageStatus(eventWithFailure); + const attrs = result.data[0].attributes as MessageStatusAttributes; + + expect(attrs.messageFailureReasonCode).toBe("DELIVERY_TIMEOUT"); + }); + + it("should exclude messageFailureReasonCode if not present", () => { + const result = transformMessageStatus(messageStatusEvent); + const attrs = result.data[0].attributes as MessageStatusAttributes; + + expect(attrs.messageFailureReasonCode).toBeUndefined(); + }); + + it("should construct message link using messageId", () => { + const result = transformMessageStatus(messageStatusEvent); + + expect(result.data[0].links.message).toBe( + "/v1/message-batches/messages/msg-789-xyz", + ); + }); + + it("should include idempotencyKey from event id in meta", () => { + const result = transformMessageStatus(messageStatusEvent); + + expect(result.data[0].meta.idempotencyKey).toBe( + "661f9510-f39c-52e5-b827-557766551111", + ); + }); + + it("should exclude operational fields (clientId, previousMessageStatus) from callback payload", () => { + const eventWithOperationalFields = { + ...messageStatusEvent, + data: { + ...messageStatusEvent.data, + previousMessageStatus: "SENDING" as MessageStatus, + }, + }; + + const result = transformMessageStatus(eventWithOperationalFields); + + // Verify that clientId and previousMessageStatus are not in the payload + expect((result.data[0].attributes as any).clientId).toBeUndefined(); + expect( + (result.data[0].attributes as any).previousMessageStatus, + ).toBeUndefined(); + }); + + it("should set type as 'MessageStatus' in data array", () => { + const result = transformMessageStatus(messageStatusEvent); + + expect(result.data[0].type).toBe("MessageStatus"); + }); + }); +}); diff --git a/lambdas/client-transform-filter-lambda/src/__tests__/validators/event-validator.test.ts b/lambdas/client-transform-filter-lambda/src/__tests__/validators/event-validator.test.ts new file mode 100644 index 0000000..c27ca03 --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/__tests__/validators/event-validator.test.ts @@ -0,0 +1,357 @@ +/* eslint-disable sonarjs/no-nested-functions */ +import { validateStatusTransitionEvent } from "services/validators/event-validator"; +import type { StatusTransitionEvent } from "models/status-transition-event"; +import type { MessageStatusData } from "models/message-status-data"; +import type { ChannelStatusData } from "models/channel-status-data"; + +type TestEvent = Omit, "traceparent"> & { + traceparent?: string; +}; + +describe("event-validator", () => { + describe("validateStatusTransitionEvent", () => { + const validMessageStatusEvent: TestEvent = { + specversion: "1.0", + id: "661f9510-f39c-52e5-b827-557766551111", + source: + "/nhs/england/notify/development/primary/data-plane/client-callbacks", + subject: + "customer/920fca11-596a-4eca-9c47-99f624614658/message/msg-789-xyz", + type: "uk.nhs.notify.client-callbacks.message.status.transitioned.v1", + time: "2026-02-05T14:30:00.000Z", + datacontenttype: "application/json", + dataschema: "https://nhs.uk/schemas/notify/message-status-data.v1.json", + traceparent: "00-4d678967f96e353c07a0a31c1849b500-07f83ba58dd8df70-01", + data: { + clientId: "client-abc-123", + messageId: "msg-789-xyz", + messageReference: "client-ref-12345", + messageStatus: "DELIVERED", + messageStatusDescription: "Message successfully delivered", + channels: [ + { + type: "NHSAPP", + channelStatus: "DELIVERED", + }, + ], + timestamp: "2026-02-05T14:29:55Z", + routingPlan: { + id: "routing-plan-123", + name: "NHS App with SMS fallback", + version: "ztoe2qRAM8M8vS0bqajhyEBcvXacrGPp", + createdDate: "2023-11-17T14:27:51.413Z", + }, + }, + }; + + it("should validate a valid message status event", () => { + expect(() => + validateStatusTransitionEvent(validMessageStatusEvent), + ).not.toThrow(); + }); + + describe("NHS Notify extension attributes validation", () => { + it("should throw error if traceparent is missing", () => { + const invalidEvent = { ...validMessageStatusEvent }; + delete invalidEvent.traceparent; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "Validation failed: traceparent: Invalid input: expected string, received undefined", + ); + }); + }); + + describe("event type namespace validation", () => { + it("should throw error if type doesn't match namespace", () => { + const invalidEvent = { + ...validMessageStatusEvent, + type: "uk.nhs.notify.wrong.namespace.v1", + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + 'Validation failed: type: Invalid option: expected one of "uk.nhs.notify.client-callbacks.message.status.transitioned.v1"|"uk.nhs.notify.client-callbacks.channel.status.transitioned.v1"', + ); + }); + }); + + describe("datacontenttype validation", () => { + it("should throw error if datacontenttype is not 'application/json'", () => { + const invalidEvent = { + ...validMessageStatusEvent, + datacontenttype: "text/plain", + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + 'Validation failed: datacontenttype: Invalid input: expected "application/json"', + ); + }); + }); + + describe("data required fields validation", () => { + it("should throw error if data.clientId is missing", () => { + const invalidEvent = { + ...validMessageStatusEvent, + data: { + ...validMessageStatusEvent.data, + clientId: undefined, + }, + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "Validation failed: clientId: Invalid input: expected string, received undefined", + ); + }); + + it("should throw error if data.messageId is missing", () => { + const invalidEvent = { + ...validMessageStatusEvent, + data: { + ...validMessageStatusEvent.data, + messageId: undefined, + }, + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "Validation failed: messageId: Invalid input: expected string, received undefined", + ); + }); + + it("should throw error if data.timestamp is missing", () => { + const invalidEvent = { + ...validMessageStatusEvent, + data: { + ...validMessageStatusEvent.data, + timestamp: undefined, + }, + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "Validation failed: timestamp: Invalid input: expected string, received undefined", + ); + }); + + it("should throw error if data.timestamp is not valid RFC 3339 format", () => { + const invalidEvent = { + ...validMessageStatusEvent, + data: { + ...validMessageStatusEvent.data, + timestamp: "2026-02-05", + }, + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "data.timestamp must be a valid RFC 3339 timestamp", + ); + }); + }); + + describe("message status specific validation", () => { + it("should throw error if messageStatus is missing for message status event", () => { + const invalidEvent = { + ...validMessageStatusEvent, + data: { + ...validMessageStatusEvent.data, + messageStatus: undefined, + }, + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "Validation failed: messageStatus: Invalid input: expected string, received undefined", + ); + }); + + it("should throw error if channels array is missing for message status event", () => { + const invalidEvent = { + ...validMessageStatusEvent, + data: { + ...validMessageStatusEvent.data, + channels: undefined, + }, + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "Validation failed: channels: Invalid input: expected array, received undefined", + ); + }); + + it("should throw error if channels array is empty", () => { + const invalidEvent = { + ...validMessageStatusEvent, + data: { + ...validMessageStatusEvent.data, + channels: [], + }, + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "data.channels must have at least one channel", + ); + }); + + it("should throw error if channel.type is missing", () => { + const invalidEvent = { + ...validMessageStatusEvent, + data: { + ...validMessageStatusEvent.data, + channels: [{ channelStatus: "delivered" } as any], + }, + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "Validation failed: channels.0.type: Invalid input: expected string, received undefined", + ); + }); + + it("should throw error if channel.channelStatus is missing", () => { + const invalidEvent = { + ...validMessageStatusEvent, + data: { + ...validMessageStatusEvent.data, + channels: [{ type: "nhsapp" } as any], + }, + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "Validation failed: channels.0.channelStatus: Invalid input: expected string, received undefined", + ); + }); + }); + + describe("channel status specific validation", () => { + const validChannelStatusEvent: TestEvent = { + ...validMessageStatusEvent, + type: "uk.nhs.notify.client-callbacks.channel.status.transitioned.v1", + data: { + clientId: "client-abc-123", + messageId: "msg-789-xyz", + messageReference: "client-ref-12345", + channel: "NHSAPP", + channelStatus: "DELIVERED", + supplierStatus: "DELIVERED", + cascadeType: "primary", + cascadeOrder: 1, + timestamp: "2026-02-05T14:29:55Z", + retryCount: 0, + }, + }; + + it("should validate a valid channel status event", () => { + expect(() => + validateStatusTransitionEvent(validChannelStatusEvent), + ).not.toThrow(); + }); + + it("should throw error if channel is missing for channel status event", () => { + const invalidEvent = { + ...validChannelStatusEvent, + data: { + ...validChannelStatusEvent.data, + channel: undefined, + }, + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "Validation failed: channel: Invalid input: expected string, received undefined", + ); + }); + + it("should throw error if channelStatus is missing for channel status event", () => { + const invalidEvent = { + ...validChannelStatusEvent, + data: { + ...validChannelStatusEvent.data, + channelStatus: undefined, + }, + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "Validation failed: channelStatus: Invalid input: expected string, received undefined", + ); + }); + + it("should throw error if supplierStatus is missing for channel status event", () => { + const invalidEvent = { + ...validChannelStatusEvent, + data: { + ...validChannelStatusEvent.data, + supplierStatus: undefined, + }, + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "Validation failed: supplierStatus: Invalid input: expected string, received undefined", + ); + }); + }); + + describe("error handling edge paths", () => { + it("should wrap CloudEvent constructor validation errors", () => { + jest.resetModules(); + + jest.isolateModules(() => { + const MockCloudEventsValidationError = + function MockCloudEventsValidationError( + this: Error, + message: string, + ) { + this.name = "ValidationError"; + this.message = message; + } as unknown as new (message: string) => Error; + + Object.setPrototypeOf( + MockCloudEventsValidationError.prototype, + Error.prototype, + ); + + jest.doMock("cloudevents", () => { + return { + CloudEvent: jest.fn(() => { + throw new MockCloudEventsValidationError("invalid CloudEvent"); + }), + ValidationError: MockCloudEventsValidationError, + }; + }); + + const moduleUnderTest = jest.requireActual( + "services/validators/event-validator", + ); + + expect(() => + moduleUnderTest.validateStatusTransitionEvent({ + specversion: "1.0", + }), + ).toThrow("CloudEvents validation failed: invalid CloudEvent"); + }); + + jest.unmock("cloudevents"); + }); + + it("should format unknown non-Error exceptions during validation", () => { + jest.resetModules(); + + jest.isolateModules(() => { + const nonErrorThrown = { foo: "bar" } as unknown as Error; + + jest.doMock("cloudevents", () => ({ + CloudEvent: jest.fn(() => { + throw nonErrorThrown; + }), + ValidationError: Error, + })); + + const moduleUnderTest = jest.requireActual( + "services/validators/event-validator", + ); + + expect(() => + moduleUnderTest.validateStatusTransitionEvent({ + specversion: "1.0", + }), + ).toThrow('Validation failed: {"foo":"bar"}'); + }); + + jest.unmock("cloudevents"); + }); + }); + }); +}); diff --git a/lambdas/client-transform-filter-lambda/src/handler.ts b/lambdas/client-transform-filter-lambda/src/handler.ts new file mode 100644 index 0000000..31dca51 --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/handler.ts @@ -0,0 +1,176 @@ +import type { SQSRecord } from "aws-lambda"; +import pMap from "p-map"; +import type { ClientCallbackPayload, StatusTransitionEvent } from "models"; +import { validateStatusTransitionEvent } from "services/validators/event-validator"; +import { transformEvent } from "services/transformers/event-transformer"; +import { extractCorrelationId } from "services/logger"; +import { ValidationError, getEventError } from "services/error-handler"; +import type { ObservabilityService } from "services/observability"; + +const BATCH_CONCURRENCY = Number(process.env.BATCH_CONCURRENCY) || 10; + +export interface TransformedEvent extends StatusTransitionEvent { + transformedPayload: ClientCallbackPayload; +} + +class BatchStats { + successful = 0; + + failed = 0; + + processed = 0; + + recordSuccess(): void { + this.successful += 1; + this.processed += 1; + } + + recordFailure(): void { + this.failed += 1; + this.processed += 1; + } + + toObject() { + return { + successful: this.successful, + failed: this.failed, + processed: this.processed, + }; + } +} + +function parseSqsMessageBody( + sqsRecord: SQSRecord, + observability: ObservabilityService, +): StatusTransitionEvent { + let parsed: any; + try { + parsed = JSON.parse(sqsRecord.body); + + observability.recordProcessingStarted({ + correlationId: extractCorrelationId(parsed), + eventType: parsed?.type, + clientId: parsed?.data?.clientId, + messageId: parsed?.data?.messageId, + }); + + validateStatusTransitionEvent(parsed); + return parsed; + } catch (error) { + if (error instanceof ValidationError) { + throw error; + } + throw new ValidationError( + `Failed to parse SQS message body as JSON: ${error instanceof Error ? error.message : "Unknown error"}`, + extractCorrelationId(parsed), + ); + } +} + +function processSingleEvent( + event: StatusTransitionEvent, + observability: ObservabilityService, +): TransformedEvent { + const correlationId = extractCorrelationId(event); + const eventType = event.type; + const { clientId, messageId } = event.data; + + observability.recordTransformationStarted({ + correlationId, + eventType, + clientId, + messageId, + }); + + const callbackPayload = transformEvent(event, correlationId); + + observability.recordCallbackGenerated( + callbackPayload, + eventType, + correlationId, + clientId, + ); + + return { + ...event, + transformedPayload: callbackPayload, + }; +} + +function recordDeliveryInitiated( + transformedEvents: TransformedEvent[], + observability: ObservabilityService, +): void { + for (const transformedEvent of transformedEvents) { + const { clientId, messageId } = transformedEvent.data; + const correlationId = extractCorrelationId(transformedEvent); + + observability.recordDeliveryInitiated({ + correlationId, + eventType: transformedEvent.type, + clientId, + messageId, + }); + } +} + +async function transformBatch( + sqsRecords: SQSRecord[], + observability: ObservabilityService, + stats: BatchStats, +): Promise { + return pMap( + sqsRecords, + (sqsRecord: SQSRecord) => { + const event = parseSqsMessageBody(sqsRecord, observability); + const correlationId = extractCorrelationId(event); + + const childObservability = observability.createChild({ + correlationId, + eventType: event.type, + clientId: event.data.clientId, + messageId: event.data.messageId, + }); + + const transformedEvent = processSingleEvent(event, childObservability); + stats.recordSuccess(); + return transformedEvent; + }, + { concurrency: BATCH_CONCURRENCY, stopOnError: true }, + ); +} + +export async function processEvents( + event: SQSRecord[], + observability: ObservabilityService, +): Promise { + const startTime = Date.now(); + const stats = new BatchStats(); + + try { + const transformedEvents = await transformBatch(event, observability, stats); + + const processingTime = Date.now() - startTime; + observability.logBatchProcessingCompleted({ + ...stats.toObject(), + batchSize: event.length, + processingTimeMs: processingTime, + }); + + recordDeliveryInitiated(transformedEvents, observability); + + await observability.flush(); + return transformedEvents; + } catch (error) { + stats.recordFailure(); + + const wrappedError = getEventError( + error, + observability.getMetrics(), + observability.getLogger(), + ); + + await observability.flush(); + throw wrappedError; + } +} diff --git a/lambdas/client-transform-filter-lambda/src/index.ts b/lambdas/client-transform-filter-lambda/src/index.ts index 91bfa94..757b83c 100644 --- a/lambdas/client-transform-filter-lambda/src/index.ts +++ b/lambdas/client-transform-filter-lambda/src/index.ts @@ -1,57 +1,34 @@ -export const handler = async (event: any) => { - // eslint-disable-next-line no-console - console.log("RAW EVENT:", JSON.stringify(event, null, 2)); +import type { SQSRecord } from "aws-lambda"; +import { Logger } from "services/logger"; +import { CallbackMetrics, createMetricLogger } from "services/metrics"; +import { ObservabilityService } from "services/observability"; +import { type TransformedEvent, processEvents } from "handler"; - let parsedEvent: any; - try { - parsedEvent = typeof event === "string" ? JSON.parse(event) : event; - } catch (error) { - // eslint-disable-next-line no-console - console.error("Could not parse event string:", error); - return { body: {} }; - } +export interface HandlerDependencies { + createObservabilityService: () => ObservabilityService; +} - let dataschemaversion: string | undefined; - let type: string | undefined; +function createDefaultObservabilityService(): ObservabilityService { + const metricsLogger = createMetricLogger(); + const metrics = new CallbackMetrics(metricsLogger); + const logger = new Logger(); - function findFields(obj: any) { - if (!obj || typeof obj !== "object") return; - if (!dataschemaversion && "dataschemaversion" in obj) - dataschemaversion = obj.dataschemaversion; - if (!type && "type" in obj) type = obj.type; + return new ObservabilityService(logger, metrics, metricsLogger); +} - for (const key of Object.keys(obj)) { - // eslint-disable-next-line security/detect-object-injection - const val = obj[key]; - if (typeof val === "string") { - try { - const nested = JSON.parse(val); - findFields(nested); - } catch { - /* empty */ - } - } else if (typeof val === "object") { - findFields(val); - } - } - } +export function createHandler( + dependencies: Partial = {}, +): (event: SQSRecord[]) => Promise { + const createObservabilityService = + dependencies.createObservabilityService ?? + createDefaultObservabilityService; - if (Array.isArray(parsedEvent)) { - for (const item of parsedEvent) findFields(item); - } else { - findFields(parsedEvent); - } + return async (event: SQSRecord[]): Promise => { + const observability = createObservabilityService(); + return processEvents(event, observability); + }; +} - if (!dataschemaversion || !type) { - // eslint-disable-next-line no-console - console.error("Failed to extract payload from event!"); - return { body: {} }; - } +export const handler = createHandler(); - return { - body: { - dataschemaversion, - type, - }, - }; -}; +export { type TransformedEvent } from "handler"; diff --git a/lambdas/client-transform-filter-lambda/src/models/channel-status-data.ts b/lambdas/client-transform-filter-lambda/src/models/channel-status-data.ts new file mode 100644 index 0000000..5a4c55f --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/models/channel-status-data.ts @@ -0,0 +1,20 @@ +import type { Channel } from "models/channel-types"; +import type { ChannelStatus, SupplierStatus } from "models/status-types"; + +export interface ChannelStatusData { + messageId: string; + messageReference: string; + channel: Channel; + channelStatus: ChannelStatus; + channelStatusDescription?: string; + channelFailureReasonCode?: string; + supplierStatus: SupplierStatus; + cascadeType: "primary" | "secondary"; + cascadeOrder: number; + timestamp: string; + retryCount: number; + + clientId: string; + previousChannelStatus?: ChannelStatus; + previousSupplierStatus?: SupplierStatus; +} diff --git a/lambdas/client-transform-filter-lambda/src/models/channel-types.ts b/lambdas/client-transform-filter-lambda/src/models/channel-types.ts new file mode 100644 index 0000000..d4526fb --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/models/channel-types.ts @@ -0,0 +1 @@ +export type Channel = "NHSAPP" | "EMAIL" | "SMS" | "LETTER"; diff --git a/lambdas/client-transform-filter-lambda/src/models/client-callback-payload.ts b/lambdas/client-transform-filter-lambda/src/models/client-callback-payload.ts new file mode 100644 index 0000000..49b663d --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/models/client-callback-payload.ts @@ -0,0 +1,55 @@ +import type { RoutingPlan } from "models/routing-plan"; +import type { Channel } from "models/channel-types"; +import type { + ChannelStatus, + MessageStatus, + SupplierStatus, +} from "models/status-types"; + +export type ClientChannel = Lowercase; +export type ClientMessageStatus = Lowercase; +export type ClientChannelStatus = Lowercase; +export type ClientSupplierStatus = Lowercase; + +export interface ClientCallbackPayload { + data: CallbackItem[]; +} + +export interface CallbackItem { + type: "MessageStatus" | "ChannelStatus"; + attributes: MessageStatusAttributes | ChannelStatusAttributes; + links: { + message: string; + }; + meta: { + idempotencyKey: string; + }; +} + +export interface MessageStatusAttributes { + messageId: string; + messageReference: string; + messageStatus: ClientMessageStatus; + messageStatusDescription?: string; + messageFailureReasonCode?: string; + channels: { + type: ClientChannel; + channelStatus: ClientChannelStatus; + }[]; + timestamp: string; + routingPlan: RoutingPlan; +} + +export interface ChannelStatusAttributes { + messageId: string; + messageReference: string; + cascadeType: "primary" | "secondary"; + cascadeOrder: number; + channel: ClientChannel; + channelStatus: ClientChannelStatus; + channelStatusDescription?: string; + channelFailureReasonCode?: string; + supplierStatus: ClientSupplierStatus; + timestamp: string; + retryCount: number; +} diff --git a/lambdas/client-transform-filter-lambda/src/models/client-config.ts b/lambdas/client-transform-filter-lambda/src/models/client-config.ts new file mode 100644 index 0000000..f24330f --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/models/client-config.ts @@ -0,0 +1,44 @@ +export type ClientSubscriptionConfiguration = ( + | MessageStatusSubscriptionConfiguration + | ChannelStatusSubscriptionConfiguration +)[]; + +interface SubscriptionConfigurationBase { + Name: string; + ClientId: string; + Description: string; + EventSource: string; + EventDetail: string; + Targets: { + Type: "API"; + TargetId: string; + Name: string; + InputTransformer: { + InputPaths: string; + InputHeaders: { + "x-hmac-sha256-signature": string; + }; + }; + InvocationEndpoint: string; + InvocationMethod: "POST"; + InvocationRateLimit: number; + APIKey: { + HeaderName: string; + HeaderValue: string; + }; + }[]; +} + +export interface MessageStatusSubscriptionConfiguration + extends SubscriptionConfigurationBase { + SubscriptionType: "MessageStatus"; + Statuses: string[]; +} + +export interface ChannelStatusSubscriptionConfiguration + extends SubscriptionConfigurationBase { + SubscriptionType: "ChannelStatus"; + ChannelType: string; + ChannelStatuses: string[]; + SupplierStatuses: string[]; +} diff --git a/lambdas/client-transform-filter-lambda/src/models/index.ts b/lambdas/client-transform-filter-lambda/src/models/index.ts new file mode 100644 index 0000000..4b76901 --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/models/index.ts @@ -0,0 +1,5 @@ +export type { ChannelStatusData } from "./channel-status-data"; +export type { MessageStatusData } from "./message-status-data"; +export type { ClientCallbackPayload } from "./client-callback-payload"; +export type { StatusTransitionEvent } from "./status-transition-event"; +export { EventTypes } from "./status-transition-event"; diff --git a/lambdas/client-transform-filter-lambda/src/models/message-status-data.ts b/lambdas/client-transform-filter-lambda/src/models/message-status-data.ts new file mode 100644 index 0000000..91e58f5 --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/models/message-status-data.ts @@ -0,0 +1,20 @@ +import type { RoutingPlan } from "models/routing-plan"; +import type { Channel } from "models/channel-types"; +import type { MessageStatus } from "models/status-types"; + +export interface MessageStatusData { + messageId: string; + messageReference: string; + messageStatus: MessageStatus; + messageStatusDescription?: string; + messageFailureReasonCode?: string; + channels: { + type: Channel; + channelStatus: string; + }[]; + timestamp: string; + routingPlan: RoutingPlan; + + clientId: string; + previousMessageStatus?: MessageStatus; +} diff --git a/lambdas/client-transform-filter-lambda/src/models/routing-plan.ts b/lambdas/client-transform-filter-lambda/src/models/routing-plan.ts new file mode 100644 index 0000000..07f348a --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/models/routing-plan.ts @@ -0,0 +1,6 @@ +export interface RoutingPlan { + id: string; + name: string; + version: string; + createdDate: string; +} diff --git a/lambdas/client-transform-filter-lambda/src/models/status-transition-event.ts b/lambdas/client-transform-filter-lambda/src/models/status-transition-event.ts new file mode 100644 index 0000000..60b1783 --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/models/status-transition-event.ts @@ -0,0 +1,29 @@ +import type { MessageStatusData } from "models/message-status-data"; +import type { ChannelStatusData } from "models/channel-status-data"; + +export interface StatusTransitionEvent< + T = MessageStatusData | ChannelStatusData, +> { + specversion: string; + id: string; + source: string; + subject: string; + type: string; + time: string; + datacontenttype: string; + dataschema: string; + traceparent: string; + + data: T; +} + +export const EventTypes = { + MESSAGE_STATUS_TRANSITIONED: + "uk.nhs.notify.client-callbacks.message.status.transitioned.v1", + CHANNEL_STATUS_TRANSITIONED: + "uk.nhs.notify.client-callbacks.channel.status.transitioned.v1", +} as const; + +export { type MessageStatusData } from "./message-status-data"; + +export { type ChannelStatusData } from "models/channel-status-data"; diff --git a/lambdas/client-transform-filter-lambda/src/models/status-types.ts b/lambdas/client-transform-filter-lambda/src/models/status-types.ts new file mode 100644 index 0000000..516bd74 --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/models/status-types.ts @@ -0,0 +1,31 @@ +export type MessageStatus = + | "CREATED" + | "PENDING_ENRICHMENT" + | "ENRICHED" + | "SENDING" + | "DELIVERED" + | "FAILED"; + +export type ChannelStatus = + | "CREATED" + | "SENDING" + | "DELIVERED" + | "FAILED" + | "SKIPPED"; + +export type SupplierStatus = + | "DELIVERED" + | "READ" + | "NOTIFICATION_ATTEMPTED" + | "UNNOTIFIED" + | "REJECTED" + | "NOTIFIED" + | "RECEIVED" + | "PERMANENT_FAILURE" + | "TEMPORARY_FAILURE" + | "TECHNICAL_FAILURE" + | "ACCEPTED" + | "CANCELLED" + | "PENDING_VIRUS_CHECK" + | "VALIDATION_FAILED" + | "UNKNOWN"; diff --git a/lambdas/client-transform-filter-lambda/src/services/callback-logger.ts b/lambdas/client-transform-filter-lambda/src/services/callback-logger.ts new file mode 100644 index 0000000..9c47e67 --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/services/callback-logger.ts @@ -0,0 +1,73 @@ +import type { + ChannelStatusAttributes, + ClientCallbackPayload, + MessageStatusAttributes, +} from "models/client-callback-payload"; +import { EventTypes } from "models/status-transition-event"; +import type { Logger } from "services/logger"; + +function isMessageStatusAttributes( + attributes: MessageStatusAttributes | ChannelStatusAttributes, + eventType: string, +): attributes is MessageStatusAttributes { + return eventType === EventTypes.MESSAGE_STATUS_TRANSITIONED; +} + +function isChannelStatusAttributes( + attributes: MessageStatusAttributes | ChannelStatusAttributes, + eventType: string, +): attributes is ChannelStatusAttributes { + return eventType === EventTypes.CHANNEL_STATUS_TRANSITIONED; +} + +function buildMessageStatusLogFields(attrs: MessageStatusAttributes) { + return { + messageStatus: attrs.messageStatus, + messageStatusDescription: attrs.messageStatusDescription, + messageFailureReasonCode: attrs.messageFailureReasonCode, + channels: attrs.channels, + }; +} + +function buildChannelStatusLogFields(attrs: ChannelStatusAttributes) { + return { + channel: attrs.channel, + channelStatus: attrs.channelStatus, + channelStatusDescription: attrs.channelStatusDescription, + channelFailureReasonCode: attrs.channelFailureReasonCode, + supplierStatus: attrs.supplierStatus, + }; +} + +export function logCallbackGenerated( + eventLogger: Logger, + payload: ClientCallbackPayload, + eventType: string, + correlationId: string | undefined, + clientId: string, +): void { + const { attributes } = payload.data[0]; + + const commonFields = { + correlationId, + callbackType: payload.data[0].type, + clientId, + messageId: attributes.messageId, + messageReference: attributes.messageReference, + }; + + let specificFields: Record; + + if (isMessageStatusAttributes(attributes, eventType)) { + specificFields = buildMessageStatusLogFields(attributes); + } else if (isChannelStatusAttributes(attributes, eventType)) { + specificFields = buildChannelStatusLogFields(attributes); + } else { + specificFields = {}; + } + + eventLogger.info("Callback generated", { + ...commonFields, + ...specificFields, + }); +} diff --git a/lambdas/client-transform-filter-lambda/src/services/error-handler.ts b/lambdas/client-transform-filter-lambda/src/services/error-handler.ts new file mode 100644 index 0000000..74a81f7 --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/services/error-handler.ts @@ -0,0 +1,181 @@ +/* eslint-disable max-classes-per-file */ + +export enum ErrorType { + VALIDATION_ERROR = "ValidationError", + CONFIG_LOADING_ERROR = "ConfigLoadingError", + TRANSFORMATION_ERROR = "TransformationError", + UNKNOWN_ERROR = "UnknownError", +} + +export class LambdaError extends Error { + public readonly errorType: ErrorType; + + public readonly correlationId?: string; + + public readonly retryable: boolean; + + constructor( + errorType: ErrorType, + message: string, + correlationId?: string, + retryable = false, + ) { + super(message); + this.name = this.constructor.name; + this.errorType = errorType; + this.correlationId = correlationId; + this.retryable = retryable; + + // Maintains proper stack trace for where our error was thrown (only available on V8) + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + } +} + +export class ValidationError extends LambdaError { + constructor(message: string, correlationId?: string) { + super(ErrorType.VALIDATION_ERROR, message, correlationId, false); + } +} + +export class ConfigLoadingError extends LambdaError { + constructor(message: string, correlationId?: string) { + super(ErrorType.CONFIG_LOADING_ERROR, message, correlationId, true); + } +} + +export class TransformationError extends LambdaError { + constructor(message: string, correlationId?: string) { + super(ErrorType.TRANSFORMATION_ERROR, message, correlationId, false); + } +} + +function serializeUnknownError(error: unknown): string { + if (typeof error === "string") { + return error; + } + + if (typeof error === "object" && error !== null) { + try { + return JSON.stringify(error); + } catch { + return "Unknown error (unable to serialize)"; + } + } + + if (typeof error === "number" || typeof error === "boolean") { + return `${error}`; + } + + return "Unknown error"; +} + +export function wrapUnknownError( + error: unknown, + correlationId?: string, +): LambdaError { + if (error instanceof LambdaError) { + return error; + } + + if (error instanceof Error) { + const wrappedError = new LambdaError( + ErrorType.UNKNOWN_ERROR, + error.message, + correlationId, + false, + ); + wrappedError.cause = error; + wrappedError.stack = error.stack; + return wrappedError; + } + + const errorMessage = serializeUnknownError(error); + + return new LambdaError( + ErrorType.UNKNOWN_ERROR, + errorMessage, + correlationId, + false, + ); +} + +export function isRetriable(error: unknown): boolean { + return error instanceof LambdaError && error.retryable; +} + +export function formatErrorForLogging(error: unknown): { + errorType: string; + message: string; + retryable: boolean; + stack?: string; +} { + if (error instanceof LambdaError) { + return { + errorType: error.errorType, + message: error.message, + retryable: error.retryable, + stack: error.stack, + }; + } + + if (error instanceof Error) { + return { + errorType: ErrorType.UNKNOWN_ERROR, + message: error.message, + retryable: false, + stack: error.stack, + }; + } + + const errorMessage = serializeUnknownError(error); + + return { + errorType: ErrorType.UNKNOWN_ERROR, + message: errorMessage, + retryable: false, + }; +} + +export function getEventError( + error: unknown, + metrics: { + emitValidationError: () => void; + emitTransformationFailure: () => void; + }, + eventLogger: { error: (message: string, context: object) => void }, + eventErrorType = "unknown", +): Error { + const correlationId = + error instanceof ValidationError || error instanceof TransformationError + ? error.correlationId + : "unknown"; + + if (error instanceof ValidationError) { + eventLogger.error("Event validation failed", { + correlationId, + error, + }); + metrics.emitValidationError(); + return error; + } + + if (error instanceof TransformationError) { + eventLogger.error("Event transformation failed", { + correlationId, + eventType: eventErrorType, + error, + }); + metrics.emitTransformationFailure(); + return error; + } + + const wrappedError = wrapUnknownError(error, correlationId); + eventLogger.error("Unexpected error processing event", { + correlationId, + error: wrappedError, + }); + metrics.emitTransformationFailure(); + return wrappedError; +} diff --git a/lambdas/client-transform-filter-lambda/src/services/logger.ts b/lambdas/client-transform-filter-lambda/src/services/logger.ts new file mode 100644 index 0000000..84b7be3 --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/services/logger.ts @@ -0,0 +1,87 @@ +import pino from "pino"; + +export interface LogContext { + correlationId?: string; + clientId?: string; + eventType?: string; + messageId?: string; + statusCode?: number; + error?: Error | string; + [key: string]: any; +} + +const basePinoLogger = pino({ + level: process.env.LOG_LEVEL || "info", + formatters: { + level: (label: string) => { + return { level: label.toUpperCase() }; + }, + }, + timestamp: () => `,"timestamp":"${new Date().toISOString()}"`, +}); + +export class Logger { + private pinoLogger: pino.Logger; + + private context: LogContext = {}; + + constructor(initialContext?: LogContext) { + if (initialContext) { + this.context = { ...initialContext }; + this.pinoLogger = basePinoLogger.child(initialContext); + } else { + this.pinoLogger = basePinoLogger; + } + } + + addContext(context: LogContext): void { + this.context = { ...this.context, ...context }; + this.pinoLogger = basePinoLogger.child(this.context); + } + + clearContext(): void { + this.context = {}; + this.pinoLogger = basePinoLogger; + } + + child(context: LogContext): Logger { + const mergedContext = { ...this.context, ...context }; + return new Logger(mergedContext); + } + + info(message: string, additionalContext?: LogContext): void { + this.pinoLogger.info(additionalContext || {}, message); + } + + warn(message: string, additionalContext?: LogContext): void { + this.pinoLogger.warn(additionalContext || {}, message); + } + + error(message: string, additionalContext?: LogContext): void { + this.pinoLogger.error(additionalContext || {}, message); + } + + debug(message: string, additionalContext?: LogContext): void { + this.pinoLogger.debug(additionalContext || {}, message); + } +} + +export const logger = new Logger(); + +export function extractCorrelationId(event: unknown): string | undefined { + if (!event || typeof event !== "object" || !("id" in event)) return undefined; + return typeof event.id === "string" ? event.id : undefined; +} + +export function logLifecycleEvent( + eventLogger: Logger, + stage: + | "processing-started" + | "transformation-started" + | "transformation-completed" + | "delivery-initiated" + | "batch-processing-completed", + context: LogContext, +): void { + eventLogger.info(`Callback lifecycle: ${stage}`, context); +} diff --git a/lambdas/client-transform-filter-lambda/src/services/metrics.ts b/lambdas/client-transform-filter-lambda/src/services/metrics.ts new file mode 100644 index 0000000..f77b487 --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/services/metrics.ts @@ -0,0 +1,42 @@ +import { Unit, createMetricsLogger } from "aws-embedded-metrics"; +import type { MetricsLogger } from "aws-embedded-metrics"; + +export const createMetricLogger = (): MetricsLogger => { + const namespace = process.env.METRICS_NAMESPACE; + const environment = process.env.ENVIRONMENT; + + if (!namespace) + throw new Error("METRICS_NAMESPACE environment variable is not set"); + if (!environment) + throw new Error("ENVIRONMENT environment variable is not set"); + + const metrics = createMetricsLogger(); + metrics.setNamespace(namespace); + metrics.setDimensions({ Environment: environment }); + + return metrics; +}; + +export class CallbackMetrics { + constructor(private readonly metrics: MetricsLogger) {} + + emitEventReceived(): void { + this.metrics.putMetric("EventsReceived", 1, Unit.Count); + } + + emitTransformationSuccess(): void { + this.metrics.putMetric("TransformationsSuccessful", 1, Unit.Count); + } + + emitTransformationFailure(): void { + this.metrics.putMetric("TransformationsFailed", 1, Unit.Count); + } + + emitDeliveryInitiated(): void { + this.metrics.putMetric("CallbacksInitiated", 1, Unit.Count); + } + + emitValidationError(): void { + this.metrics.putMetric("ValidationErrors", 1, Unit.Count); + } +} diff --git a/lambdas/client-transform-filter-lambda/src/services/observability.ts b/lambdas/client-transform-filter-lambda/src/services/observability.ts new file mode 100644 index 0000000..cf0e7b2 --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/services/observability.ts @@ -0,0 +1,94 @@ +import type { MetricsLogger } from "aws-embedded-metrics"; +import type { ClientCallbackPayload } from "models"; +import { logCallbackGenerated } from "services/callback-logger"; +import type { Logger } from "services/logger"; +import { logLifecycleEvent } from "services/logger"; +import type { CallbackMetrics } from "services/metrics"; + +export class ObservabilityService { + constructor( + private readonly logger: Logger, + private readonly metrics: CallbackMetrics, + private readonly metricsLogger: MetricsLogger, + ) {} + + getLogger(): Logger { + return this.logger; + } + + getMetrics(): CallbackMetrics { + return this.metrics; + } + + recordProcessingStarted(context: { + correlationId?: string; + eventType?: string; + clientId?: string; + messageId?: string; + }): void { + logLifecycleEvent(this.logger, "processing-started", context); + this.metrics.emitEventReceived(); + } + + recordTransformationStarted(context: { + correlationId?: string; + eventType: string; + clientId: string; + messageId: string; + }): void { + logLifecycleEvent(this.logger, "transformation-started", context); + } + + logBatchProcessingCompleted(context: { + successful: number; + failed: number; + processed: number; + batchSize: number; + processingTimeMs: number; + }): void { + logLifecycleEvent(this.logger, "batch-processing-completed", context); + } + + recordDeliveryInitiated(context: { + correlationId?: string; + eventType: string; + clientId: string; + messageId: string; + }): void { + logLifecycleEvent(this.logger, "delivery-initiated", context); + this.metrics.emitDeliveryInitiated(); + } + + recordCallbackGenerated( + payload: ClientCallbackPayload, + eventType: string, + correlationId: string | undefined, + clientId: string, + ): void { + logCallbackGenerated( + this.logger, + payload, + eventType, + correlationId, + clientId, + ); + this.metrics.emitTransformationSuccess(); + } + + createChild(context: { + correlationId?: string; + eventType: string; + clientId: string; + messageId: string; + }): ObservabilityService { + return new ObservabilityService( + this.logger.child(context), + this.metrics, + this.metricsLogger, + ); + } + + async flush(): Promise { + await this.metricsLogger.flush(); + } +} diff --git a/lambdas/client-transform-filter-lambda/src/services/transformers/channel-status-transformer.ts b/lambdas/client-transform-filter-lambda/src/services/transformers/channel-status-transformer.ts new file mode 100644 index 0000000..5ee5864 --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/services/transformers/channel-status-transformer.ts @@ -0,0 +1,58 @@ +import type { StatusTransitionEvent } from "models/status-transition-event"; +import type { ChannelStatusData } from "models/channel-status-data"; +import type { + ChannelStatusAttributes, + ClientCallbackPayload, + ClientChannel, + ClientChannelStatus, + ClientSupplierStatus, +} from "models/client-callback-payload"; + +export function transformChannelStatus( + event: StatusTransitionEvent, +): ClientCallbackPayload { + const notifyData = event.data; + const { messageId } = notifyData; + const channel = notifyData.channel.toLowerCase() as ClientChannel; + const channelStatus = + notifyData.channelStatus.toLowerCase() as ClientChannelStatus; + const supplierStatus = + notifyData.supplierStatus.toLowerCase() as ClientSupplierStatus; + + const attributes: ChannelStatusAttributes = { + messageId: notifyData.messageId, + messageReference: notifyData.messageReference, + channel, + channelStatus, + supplierStatus, + cascadeType: notifyData.cascadeType, + cascadeOrder: notifyData.cascadeOrder, + timestamp: notifyData.timestamp, + retryCount: notifyData.retryCount, + }; + + if (notifyData.channelStatusDescription) { + attributes.channelStatusDescription = notifyData.channelStatusDescription; + } + + if (notifyData.channelFailureReasonCode) { + attributes.channelFailureReasonCode = notifyData.channelFailureReasonCode; + } + + const payload: ClientCallbackPayload = { + data: [ + { + type: "ChannelStatus", + attributes, + links: { + message: `/v1/message-batches/messages/${messageId}`, + }, + meta: { + idempotencyKey: event.id, + }, + }, + ], + }; + + return payload; +} diff --git a/lambdas/client-transform-filter-lambda/src/services/transformers/event-transformer.ts b/lambdas/client-transform-filter-lambda/src/services/transformers/event-transformer.ts new file mode 100644 index 0000000..6d5c15e --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/services/transformers/event-transformer.ts @@ -0,0 +1,32 @@ +import type { + ChannelStatusData, + ClientCallbackPayload, + MessageStatusData, + StatusTransitionEvent, +} from "models"; +import { EventTypes } from "models"; +import { TransformationError } from "services/error-handler"; +import { transformChannelStatus } from "services/transformers/channel-status-transformer"; +import { transformMessageStatus } from "services/transformers/message-status-transformer"; + +export function transformEvent( + rawEvent: StatusTransitionEvent, + correlationId: string | undefined, +): ClientCallbackPayload { + const eventType = rawEvent.type; + + if (eventType === EventTypes.MESSAGE_STATUS_TRANSITIONED) { + const typedEvent = rawEvent as StatusTransitionEvent; + return transformMessageStatus(typedEvent); + } + + if (eventType === EventTypes.CHANNEL_STATUS_TRANSITIONED) { + const typedEvent = rawEvent as StatusTransitionEvent; + return transformChannelStatus(typedEvent); + } + + throw new TransformationError( + `Unsupported event type: ${eventType}`, + correlationId, + ); +} diff --git a/lambdas/client-transform-filter-lambda/src/services/transformers/message-status-transformer.ts b/lambdas/client-transform-filter-lambda/src/services/transformers/message-status-transformer.ts new file mode 100644 index 0000000..ba34e3d --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/services/transformers/message-status-transformer.ts @@ -0,0 +1,59 @@ +import type { StatusTransitionEvent } from "models/status-transition-event"; +import type { MessageStatusData } from "models/message-status-data"; +import type { + ClientCallbackPayload, + ClientChannel, + ClientChannelStatus, + ClientMessageStatus, + MessageStatusAttributes, +} from "models/client-callback-payload"; + +export function transformMessageStatus( + event: StatusTransitionEvent, +): ClientCallbackPayload { + const notifyData = event.data; + const { messageId } = notifyData; + const messageStatus = + notifyData.messageStatus.toLowerCase() as ClientMessageStatus; + const channels = notifyData.channels.map( + (channel: { type: string; channelStatus: string }) => ({ + ...channel, + type: channel.type.toLowerCase() as ClientChannel, + channelStatus: channel.channelStatus.toLowerCase() as ClientChannelStatus, + }), + ); + + const attributes: MessageStatusAttributes = { + messageId: notifyData.messageId, + messageReference: notifyData.messageReference, + messageStatus, + channels, + timestamp: notifyData.timestamp, + routingPlan: notifyData.routingPlan, + }; + + if (notifyData.messageStatusDescription) { + attributes.messageStatusDescription = notifyData.messageStatusDescription; + } + + if (notifyData.messageFailureReasonCode) { + attributes.messageFailureReasonCode = notifyData.messageFailureReasonCode; + } + + const payload: ClientCallbackPayload = { + data: [ + { + type: "MessageStatus", + attributes, + links: { + message: `/v1/message-batches/messages/${messageId}`, + }, + meta: { + idempotencyKey: event.id, + }, + }, + ], + }; + + return payload; +} diff --git a/lambdas/client-transform-filter-lambda/src/services/validators/event-validator.ts b/lambdas/client-transform-filter-lambda/src/services/validators/event-validator.ts new file mode 100644 index 0000000..dbd7bb8 --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/services/validators/event-validator.ts @@ -0,0 +1,98 @@ +import { + CloudEvent, + ValidationError as CloudEventsValidationError, +} from "cloudevents"; +import { z } from "zod"; +import { + EventTypes, + StatusTransitionEvent, +} from "models/status-transition-event"; +import { ValidationError } from "services/error-handler"; +import { extractCorrelationId } from "services/logger"; + +const NHSNotifyExtensionsSchema = z.object({ + traceparent: z.string().min(1), +}); + +const EventConstraintsSchema = z.object({ + type: z.enum([ + EventTypes.MESSAGE_STATUS_TRANSITIONED, + EventTypes.CHANNEL_STATUS_TRANSITIONED, + ]), + datacontenttype: z.literal("application/json"), + data: z.unknown(), +}); + +const BaseDataSchema = z.object({ + clientId: z.string().min(1), + messageId: z.string().min(1), + timestamp: z + .string() + .min(1) + .pipe(z.iso.datetime("data.timestamp must be a valid RFC 3339 timestamp")), +}); + +const MessageStatusDataSchema = BaseDataSchema.extend({ + messageStatus: z.string().min(1), + channels: z + .array( + z.object({ + type: z.string().min(1), + channelStatus: z.string().min(1), + }), + ) + .min(1, "data.channels must have at least one channel"), +}); + +const ChannelStatusDataSchema = BaseDataSchema.extend({ + channel: z.string().min(1), + channelStatus: z.string().min(1), + supplierStatus: z.string().min(1), +}); + +function isMessageStatusEvent(type: string): boolean { + return type === EventTypes.MESSAGE_STATUS_TRANSITIONED; +} + +function isChannelStatusEvent(type: string): boolean { + return type === EventTypes.CHANNEL_STATUS_TRANSITIONED; +} + +function formatValidationError(error: unknown, event: unknown): never { + const correlationId = extractCorrelationId(event); + + let message: string; + if (error instanceof CloudEventsValidationError) { + message = `CloudEvents validation failed: ${error.message}`; + } else if (error instanceof z.ZodError) { + const issues = error.issues + .map((issue) => `${issue.path.join(".")}: ${issue.message}`) + .join(", "); + message = `Validation failed: ${issues}`; + } else if (error instanceof Error) { + message = error.message; + } else { + message = `Validation failed: ${JSON.stringify(error)}`; + } + + throw new ValidationError(message, correlationId); +} + +export function validateStatusTransitionEvent( + event: unknown, +): asserts event is StatusTransitionEvent { + try { + const ce = new CloudEvent(event as any, true); + + NHSNotifyExtensionsSchema.parse(event); + EventConstraintsSchema.parse(event); + + if (isMessageStatusEvent(ce.type)) { + MessageStatusDataSchema.parse(ce.data); + } else if (isChannelStatusEvent(ce.type)) { + ChannelStatusDataSchema.parse(ce.data); + } + } catch (error) { + formatValidationError(error, event); + } +} diff --git a/lambdas/client-transform-filter-lambda/tsconfig.json b/lambdas/client-transform-filter-lambda/tsconfig.json index 9e05f63..64297cf 100644 --- a/lambdas/client-transform-filter-lambda/tsconfig.json +++ b/lambdas/client-transform-filter-lambda/tsconfig.json @@ -1,4 +1,8 @@ { + "compilerOptions": { + "baseUrl": "src", + "isolatedModules": true + }, "extends": "../../tsconfig.base.json", "include": [ "src/**/*", diff --git a/lambdas/mock-webhook-lambda/.gitignore b/lambdas/mock-webhook-lambda/.gitignore new file mode 100644 index 0000000..80323f7 --- /dev/null +++ b/lambdas/mock-webhook-lambda/.gitignore @@ -0,0 +1,4 @@ +coverage +node_modules +dist +.reports diff --git a/lambdas/mock-webhook-lambda/jest.config.ts b/lambdas/mock-webhook-lambda/jest.config.ts new file mode 100644 index 0000000..3593e58 --- /dev/null +++ b/lambdas/mock-webhook-lambda/jest.config.ts @@ -0,0 +1,17 @@ +import { nodeJestConfig } from "../../jest.config.base"; + +export default { + ...nodeJestConfig, + coverageThreshold: { + global: { + branches: 80, + functions: 100, + lines: 100, + statements: -10, + }, + }, + coveragePathIgnorePatterns: [ + ...(nodeJestConfig.coveragePathIgnorePatterns ?? []), + "zod-validators.ts", + ], +}; diff --git a/lambdas/mock-webhook-lambda/package.json b/lambdas/mock-webhook-lambda/package.json new file mode 100644 index 0000000..f7584a2 --- /dev/null +++ b/lambdas/mock-webhook-lambda/package.json @@ -0,0 +1,26 @@ +{ + "dependencies": { + "esbuild": "^0.25.0", + "pino": "^9.5.0" + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.2", + "@types/aws-lambda": "^8.10.148", + "@types/jest": "^29.5.14", + "@types/node": "^22.0.0", + "jest": "^29.7.0", + "jest-html-reporter": "^3.10.2", + "ts-jest": "^29.2.5", + "typescript": "^5.8.2" + }, + "name": "nhs-notify-mock-webhook-lambda", + "private": true, + "scripts": { + "lambda-build": "rm -rf dist && npx esbuild --bundle --minify --sourcemap --target=es2020 --platform=node --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/mock-webhook-lambda/src/__tests__/index.test.ts b/lambdas/mock-webhook-lambda/src/__tests__/index.test.ts new file mode 100644 index 0000000..3bda884 --- /dev/null +++ b/lambdas/mock-webhook-lambda/src/__tests__/index.test.ts @@ -0,0 +1,294 @@ +import type { APIGatewayProxyEvent } from "aws-lambda"; +import { handler } from "index"; +import type { CallbackMessage, CallbackPayload } from "types"; + +jest.mock("pino", () => { + const info = jest.fn(); + const error = jest.fn(); + const mockPino = jest.fn(() => ({ + info, + error, + })); + + return { + __esModule: true, + default: mockPino, + info, + error, + }; +}); + +const createMockEvent = (body: string | null): APIGatewayProxyEvent => ({ + body, + headers: {}, + multiValueHeaders: {}, + httpMethod: "POST", + isBase64Encoded: false, + path: "/webhook", + pathParameters: null, + queryStringParameters: null, + multiValueQueryStringParameters: null, + stageVariables: null, + requestContext: { + accountId: "123456789012", + apiId: "test-api", + protocol: "HTTP/1.1", + httpMethod: "POST", + path: "/webhook", + stage: "test", + requestId: "test-request-id", + requestTime: "01/Jan/2026:00:00:00 +0000", + requestTimeEpoch: 1_735_689_600_000, + identity: { + accessKey: null, + accountId: null, + apiKey: null, + apiKeyId: null, + caller: null, + clientCert: null, + cognitoAuthenticationProvider: null, + cognitoAuthenticationType: null, + cognitoIdentityId: null, + cognitoIdentityPoolId: null, + principalOrgId: null, + sourceIp: "127.0.0.1", + user: null, + userAgent: "test-agent", + userArn: null, + }, + authorizer: null, + domainName: "test.execute-api.eu-west-2.amazonaws.com", + domainPrefix: "test", + resourceId: "test-resource", + resourcePath: "/webhook", + }, + resource: "/webhook", +}); + +describe("Mock Webhook Lambda", () => { + describe("Happy Path", () => { + it("should accept and log MessageStatus callback", async () => { + const callback: CallbackMessage = { + data: [ + { + type: "MessageStatus", + id: "msg-123", + attributes: { + messageId: "msg-123", + messageReference: "ref-456", + messageStatus: "delivered", + timestamp: "2026-01-01T00:00:00Z", + }, + }, + ], + }; + + const event = createMockEvent(JSON.stringify(callback)); + const result = await handler(event); + + expect(result.statusCode).toBe(200); + const body = JSON.parse(result.body); + expect(body.message).toBe("Callback received"); + expect(body.receivedCount).toBe(1); + }); + + it("should accept and log ChannelStatus callback", async () => { + const callback: CallbackMessage = { + data: [ + { + type: "ChannelStatus", + id: "msg-123", + attributes: { + messageId: "msg-123", + messageReference: "ref-456", + channel: "nhsapp", + channelStatus: "delivered", + supplierStatus: "delivered", + timestamp: "2026-01-01T00:00:00Z", + }, + }, + ], + }; + + const event = createMockEvent(JSON.stringify(callback)); + const result = await handler(event); + + expect(result.statusCode).toBe(200); + const body = JSON.parse(result.body); + expect(body.message).toBe("Callback received"); + expect(body.receivedCount).toBe(1); + }); + + it("should accept multiple callbacks in one request", async () => { + const callback: CallbackMessage = { + data: [ + { + type: "MessageStatus", + id: "msg-123", + attributes: { + messageId: "msg-123", + messageStatus: "pending", + }, + }, + { + type: "MessageStatus", + id: "msg-123", + attributes: { + messageId: "msg-123", + messageStatus: "delivered", + }, + }, + ], + }; + + const event = createMockEvent(JSON.stringify(callback)); + const result = await handler(event); + + expect(result.statusCode).toBe(200); + const body = JSON.parse(result.body); + expect(body.message).toBe("Callback received"); + expect(body.receivedCount).toBe(2); + }); + }); + + describe("Error Handling", () => { + it("should return 400 when body is null", async () => { + const event = createMockEvent(null); + const result = await handler(event); + + expect(result.statusCode).toBe(400); + const body = JSON.parse(result.body); + expect(body.message).toBe("No body"); + }); + + it("should return 400 when body is empty string", async () => { + const event = createMockEvent(""); + const result = await handler(event); + + expect(result.statusCode).toBe(400); + const body = JSON.parse(result.body); + expect(body.message).toBe("No body"); + }); + + it("should return 400 when body is invalid JSON", async () => { + const event = createMockEvent("invalid json {"); + const result = await handler(event); + + expect(result.statusCode).toBe(400); + const body = JSON.parse(result.body); + expect(body.message).toBe("Invalid JSON body"); + }); + + it("should return 400 when data field is missing", async () => { + const event = createMockEvent(JSON.stringify({ notData: [] })); + const result = await handler(event); + + expect(result.statusCode).toBe(400); + const body = JSON.parse(result.body); + expect(body.message).toBe("Invalid message structure"); + }); + + it("should return 400 when data field is not an array", async () => { + const event = createMockEvent(JSON.stringify({ data: "not-array" })); + const result = await handler(event); + + expect(result.statusCode).toBe(400); + const body = JSON.parse(result.body); + expect(body.message).toBe("Invalid message structure"); + }); + + it("should return 400 when callback payload is missing attributes", async () => { + const event = createMockEvent( + JSON.stringify({ data: [{ type: "MessageStatus", id: "msg-123" }] }), + ); + const result = await handler(event); + + expect(result.statusCode).toBe(400); + const body = JSON.parse(result.body); + expect(body.message).toBe("Invalid message structure"); + }); + + it("should return 400 when callback payload type is invalid", async () => { + const event = createMockEvent( + JSON.stringify({ + data: [{ type: "OtherStatus", id: "msg-123", attributes: {} }], + }), + ); + const result = await handler(event); + + expect(result.statusCode).toBe(400); + const body = JSON.parse(result.body); + expect(body.message).toBe("Invalid message structure"); + }); + + it("should return 400 when callback payload item is an array", async () => { + const event = createMockEvent( + JSON.stringify({ data: [["invalid-payload"]] }), + ); + const result = await handler(event); + + expect(result.statusCode).toBe(400); + const body = JSON.parse(result.body); + expect(body.message).toBe("Invalid message structure"); + }); + + it("should return 500 when parsing throws non-syntax error", async () => { + const parseSpy = jest.spyOn(JSON, "parse").mockImplementationOnce(() => { + throw new Error("forced-parse-error"); + }); + + const event = createMockEvent('{"data":[]}'); + const result = await handler(event); + + expect(result.statusCode).toBe(500); + const body = JSON.parse(result.body); + expect(body.message).toBe("Internal server error"); + + parseSpy.mockRestore(); + }); + }); + + describe("Logging", () => { + it("should log callback with structured format including messageId", async () => { + const callback: CallbackMessage = { + data: [ + { + type: "MessageStatus", + id: "test-msg-789", + attributes: { + messageId: "test-msg-789", + messageStatus: "delivered", + }, + }, + ], + }; + + const event = createMockEvent(JSON.stringify(callback)); + + await handler(event); + + const logger = jest.requireMock("pino"); + const infoCalls = logger.info.mock.calls as unknown[][]; + + expect(logger).toBeDefined(); + + const callbackLog = infoCalls + .map(([payload]: unknown[]) => payload) + .find( + (payload: unknown) => + typeof payload === "object" && + payload !== null && + "msg" in payload && + payload.msg === + 'CALLBACK test-msg-789 MessageStatus : {"type":"MessageStatus","id":"test-msg-789","attributes":{"messageId":"test-msg-789","messageStatus":"delivered"}}', + ); + + expect(callbackLog).toBeDefined(); + expect(callbackLog).toMatchObject({ + messageId: "test-msg-789", + messageType: "MessageStatus", + correlationId: "test-request-id", + }); + }); + }); +}); diff --git a/lambdas/mock-webhook-lambda/src/index.ts b/lambdas/mock-webhook-lambda/src/index.ts new file mode 100644 index 0000000..656590f --- /dev/null +++ b/lambdas/mock-webhook-lambda/src/index.ts @@ -0,0 +1,149 @@ +import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda"; +import pino from "pino"; +import type { CallbackMessage, CallbackPayload, LambdaResponse } from "types"; + +const logger = pino({ + level: process.env.LOG_LEVEL || "info", +}); + +function isValidCallbackPayload(payload: unknown): payload is CallbackPayload { + if ( + typeof payload !== "object" || + payload === null || + Array.isArray(payload) + ) { + return false; + } + + const candidate = payload as { + type?: unknown; + attributes?: unknown; + }; + + return ( + (candidate.type === "MessageStatus" || + candidate.type === "ChannelStatus") && + typeof candidate.attributes === "object" && + candidate.attributes !== null && + !Array.isArray(candidate.attributes) && + typeof (candidate.attributes as Record).messageId === + "string" + ); +} + +export async function handler( + event: APIGatewayProxyEvent, +): Promise { + logger.info({ event }, "Received event"); + + let correlationId = "unknown"; + + logger.info({ + correlationId, + msg: "Mock webhook invoked", + path: event.path, + method: event.httpMethod, + }); + + if (!event.body) { + logger.error({ + correlationId, + msg: "No event body received", + }); + + const response: LambdaResponse = { + message: "No body", + }; + + return { + statusCode: 400, + body: JSON.stringify(response), + }; + } + + try { + const messages = JSON.parse(event.body) as CallbackMessage; + + const firstItemId = messages?.data?.[0]?.attributes?.messageId; + if (typeof firstItemId === "string" && firstItemId) { + correlationId = firstItemId; + } + + if (!messages.data || !Array.isArray(messages.data)) { + logger.error({ + correlationId, + msg: "Invalid message structure - missing or invalid data array", + }); + + return { + statusCode: 400, + body: JSON.stringify({ message: "Invalid message structure" }), + }; + } + + if (!messages.data.every((payload) => isValidCallbackPayload(payload))) { + logger.error({ + correlationId, + msg: "Invalid message structure - invalid callback payload", + }); + + return { + statusCode: 400, + body: JSON.stringify({ message: "Invalid message structure" }), + }; + } + + // Log each callback in a format that can be queried from CloudWatch + for (const message of messages.data) { + const messageId = message.attributes.messageId as string | undefined; + const messageType = message.type; + + logger.info({ + correlationId, + messageId, + messageType, + msg: `CALLBACK ${messageId} ${messageType} : ${JSON.stringify(message)}`, + }); + } + + const response: LambdaResponse = { + message: "Callback received", + receivedCount: messages.data.length, + }; + + logger.info({ + correlationId, + receivedCount: messages.data.length, + msg: "Callbacks logged successfully", + }); + + return { + statusCode: 200, + body: JSON.stringify(response), + }; + } catch (error) { + if (error instanceof SyntaxError) { + logger.error({ + correlationId, + error: error.message, + msg: "Invalid JSON body", + }); + + return { + statusCode: 400, + body: JSON.stringify({ message: "Invalid JSON body" }), + }; + } + + logger.error({ + correlationId, + error: error instanceof Error ? error.message : String(error), + msg: "Failed to process callback", + }); + + return { + statusCode: 500, + body: JSON.stringify({ message: "Internal server error" }), + }; + } +} diff --git a/lambdas/mock-webhook-lambda/src/types.ts b/lambdas/mock-webhook-lambda/src/types.ts new file mode 100644 index 0000000..e08bc92 --- /dev/null +++ b/lambdas/mock-webhook-lambda/src/types.ts @@ -0,0 +1,31 @@ +/** + * JSON:API message wrapper containing callback data + */ +export interface CallbackMessage { + data: T[]; +} + +/** + * JSON:API callback payload (MessageStatus or ChannelStatus) + */ +export interface CallbackPayload { + type: "MessageStatus" | "ChannelStatus"; + attributes: { + messageId: string; + [key: string]: unknown; + }; + links: { + message: string; + }; + meta: { + idempotencyKey: string; + }; +} + +/** + * Lambda response structure + */ +export interface LambdaResponse { + message: string; + receivedCount?: number; +} diff --git a/lambdas/mock-webhook-lambda/tsconfig.json b/lambdas/mock-webhook-lambda/tsconfig.json new file mode 100644 index 0000000..64297cf --- /dev/null +++ b/lambdas/mock-webhook-lambda/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "baseUrl": "src", + "isolatedModules": true + }, + "extends": "../../tsconfig.base.json", + "include": [ + "src/**/*", + "jest.config.ts" + ] +} diff --git a/package-lock.json b/package-lock.json index aa3caa0..2a6358d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,9 @@ "": { "name": "nhs-notify-client-callbacks", "workspaces": [ - "lambdas/client-transform-filter-lambda" + "lambdas/client-transform-filter-lambda", + "lambdas/mock-webhook-lambda", + "tests/integration" ], "devDependencies": { "@stylistic/eslint-plugin": "^3.1.0", @@ -45,7 +47,12 @@ "name": "nhs-notify-client-transform-filter-lambda", "version": "0.0.1", "dependencies": { - "esbuild": "^0.25.0" + "aws-embedded-metrics": "^4.2.1", + "cloudevents": "^8.0.2", + "esbuild": "^0.25.0", + "p-map": "^4.0.0", + "pino": "^9.6.0", + "zod": "^4.1.13" }, "devDependencies": { "@tsconfig/node22": "^22.0.2", @@ -56,6 +63,30 @@ "typescript": "^5.8.2" } }, + "lambdas/client-transform-filter-lambda/node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "lambdas/client-transform-filter-lambda/node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "lambdas/example-lambda": { "name": "nhs-notify-repository-template-example-lambda", "version": "0.0.1", @@ -72,6 +103,41 @@ "typescript": "^5.8.2" } }, + "lambdas/mock-webhook-lambda": { + "name": "nhs-notify-mock-webhook-lambda", + "version": "0.0.1", + "dependencies": { + "esbuild": "^0.25.0", + "pino": "^9.5.0" + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.2", + "@types/aws-lambda": "^8.10.148", + "@types/jest": "^29.5.14", + "@types/node": "^22.0.0", + "jest": "^29.7.0", + "jest-html-reporter": "^3.10.2", + "ts-jest": "^29.2.5", + "typescript": "^5.8.2" + } + }, + "lambdas/mock-webhook-lambda/node_modules/@types/node": { + "version": "22.19.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", + "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "lambdas/mock-webhook-lambda/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "dev": true, @@ -84,1686 +150,3221 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" } }, - "node_modules/@babel/compat-data": { - "version": "7.27.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" } }, - "node_modules/@babel/core": { - "version": "7.27.4", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.4", - "@babel/parser": "^7.27.4", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.27.4", - "@babel/types": "^7.27.3", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" + "node": ">=14.0.0" } }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" } }, - "node_modules/@babel/generator": { - "version": "7.27.5", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", "dependencies": { - "@babel/parser": "^7.27.5", - "@babel/types": "^7.27.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" } }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" } }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/client-cloudwatch-logs": { + "version": "3.991.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.991.0.tgz", + "integrity": "sha512-6r4aQSRiEDD2DLX5dfilTDVgMrGYW3sxr7ZgOV1t+nmHueHpEX4zgNAQyEkH0fstYcEfVMDA2O/uEbscgXgtIw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.10", + "@aws-sdk/credential-provider-node": "^3.972.9", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.10", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.991.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.8", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.23.0", + "@smithy/eventstream-serde-browser": "^4.2.8", + "@smithy/eventstream-serde-config-resolver": "^4.3.8", + "@smithy/eventstream-serde-node": "^4.2.8", + "@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.14", + "@smithy/middleware-retry": "^4.4.31", + "@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.10", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.3", + "@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.30", + "@smithy/util-defaults-mode-node": "^4.2.33", + "@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-cloudwatch-logs/node_modules/@aws-sdk/util-endpoints": { + "version": "3.991.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.991.0.tgz", + "integrity": "sha512-m8tcZ3SbqG3NRDv0Py3iBKdb4/FlpOCP4CQ6wRtsk4vs3UypZ0nFdZwCRVnTN7j+ldj+V72xVi/JBlxFBDE7Sg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.9.0" + "node": ">=20.0.0" } }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/client-eventbridge": { + "version": "3.990.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-eventbridge/-/client-eventbridge-3.990.0.tgz", + "integrity": "sha512-ed6BHW+1Bk/9dQAlcNWOw06aq/ZZZGf3wSbpFdNyI6m7vl7pixo4vcelaGSH0iQUfaMqJoQPNmTEuo2iTvYTRQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.10", + "@aws-sdk/credential-provider-node": "^3.972.9", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.10", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/signature-v4-multi-region": "3.990.0", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.990.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.8", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.23.0", + "@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.14", + "@smithy/middleware-retry": "^4.4.31", + "@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.10", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.3", + "@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.30", + "@smithy/util-defaults-mode-node": "^4.2.33", + "@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-sqs": { + "version": "3.990.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sqs/-/client-sqs-3.990.0.tgz", + "integrity": "sha512-ItlHYqVAM62ua0bnPTsKOXwXcBoCLNcZ1Ts36Q/ff8aIx1wF8KUBc62lvT4agSp+HNDWTvk0ATI74Ru9dvj17g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.10", + "@aws-sdk/credential-provider-node": "^3.972.9", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-sdk-sqs": "^3.972.7", + "@aws-sdk/middleware-user-agent": "^3.972.10", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.990.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.8", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.23.0", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/md5-js": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.14", + "@smithy/middleware-retry": "^4.4.31", + "@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.10", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.3", + "@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.30", + "@smithy/util-defaults-mode-node": "^4.2.33", + "@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-sso": { + "version": "3.990.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.990.0.tgz", + "integrity": "sha512-xTEaPjZwOqVjGbLOP7qzwbdOWJOo1ne2mUhTZwEBBkPvNk4aXB/vcYwWwrjoSWUqtit4+GDbO75ePc/S6TUJYQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.10", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.10", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.990.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.8", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.23.0", + "@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.14", + "@smithy/middleware-retry": "^4.4.31", + "@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.10", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.3", + "@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.30", + "@smithy/util-defaults-mode-node": "^4.2.33", + "@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/core": { + "version": "3.973.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.10.tgz", + "integrity": "sha512-4u/FbyyT3JqzfsESI70iFg6e2yp87MB5kS2qcxIA66m52VSTN1fvuvbCY1h/LKq1LvuxIrlJ1ItcyjvcKoaPLg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/xml-builder": "^3.972.4", + "@smithy/core": "^3.23.0", + "@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.11.3", + "@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": ">=6.9.0" + "node": ">=20.0.0" } }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.8.tgz", + "integrity": "sha512-r91OOPAcHnLCSxaeu/lzZAVRCZ/CtTNuwmJkUwpwSDshUrP7bkX1OmFn2nUMWd9kN53Q4cEo8b7226G4olt2Mg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.10", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.9.0" + "node": ">=20.0.0" } }, - "node_modules/@babel/helpers": { - "version": "7.27.6", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.10.tgz", + "integrity": "sha512-DTtuyXSWB+KetzLcWaSahLJCtTUe/3SXtlGp4ik9PCe9xD6swHEkG8n8/BNsQ9dsihb9nhFvuUB4DpdBGDcvVg==", + "license": "Apache-2.0", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.27.6" + "@aws-sdk/core": "^3.973.10", + "@aws-sdk/types": "^3.973.1", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/node-http-handler": "^4.4.10", + "@smithy/property-provider": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.3", + "@smithy/types": "^4.12.0", + "@smithy/util-stream": "^4.5.12", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=20.0.0" } }, - "node_modules/@babel/parser": { - "version": "7.27.5", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.8.tgz", + "integrity": "sha512-n2dMn21gvbBIEh00E8Nb+j01U/9rSqFIamWRdGm/mE5e+vHQ9g0cBNdrYFlM6AAiryKVHZmShWT9D1JAWJ3ISw==", + "license": "Apache-2.0", "dependencies": { - "@babel/types": "^7.27.3" - }, - "bin": { - "parser": "bin/babel-parser.js" + "@aws-sdk/core": "^3.973.10", + "@aws-sdk/credential-provider-env": "^3.972.8", + "@aws-sdk/credential-provider-http": "^3.972.10", + "@aws-sdk/credential-provider-login": "^3.972.8", + "@aws-sdk/credential-provider-process": "^3.972.8", + "@aws-sdk/credential-provider-sso": "^3.972.8", + "@aws-sdk/credential-provider-web-identity": "^3.972.8", + "@aws-sdk/nested-clients": "3.990.0", + "@aws-sdk/types": "^3.973.1", + "@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/credential-provider-login": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.8.tgz", + "integrity": "sha512-rMFuVids8ICge/X9DF5pRdGMIvkVhDV9IQFQ8aTYk6iF0rl9jOUa1C3kjepxiXUlpgJQT++sLZkT9n0TMLHhQw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.10", + "@aws-sdk/nested-clients": "3.990.0", + "@aws-sdk/types": "^3.973.1", + "@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": ">=6.0.0" + "node": ">=20.0.0" } }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.9.tgz", + "integrity": "sha512-LfJfO0ClRAq2WsSnA9JuUsNyIicD2eyputxSlSL0EiMrtxOxELLRG6ZVYDf/a1HCepaYPXeakH4y8D5OLCauag==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@aws-sdk/credential-provider-env": "^3.972.8", + "@aws-sdk/credential-provider-http": "^3.972.10", + "@aws-sdk/credential-provider-ini": "^3.972.8", + "@aws-sdk/credential-provider-process": "^3.972.8", + "@aws-sdk/credential-provider-sso": "^3.972.8", + "@aws-sdk/credential-provider-web-identity": "^3.972.8", + "@aws-sdk/types": "^3.973.1", + "@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" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.8.tgz", + "integrity": "sha512-6cg26ffFltxM51OOS8NH7oE41EccaYiNlbd5VgUYwhiGCySLfHoGuGrLm2rMB4zhy+IO5nWIIG0HiodX8zdvHA==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@aws-sdk/core": "^3.973.10", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.8.tgz", + "integrity": "sha512-35kqmFOVU1n26SNv+U37sM8b2TzG8LyqAcd6iM9gprqxyHEh/8IM3gzN4Jzufs3qM6IrH8e43ryZWYdvfVzzKQ==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" + "@aws-sdk/client-sso": "3.990.0", + "@aws-sdk/core": "^3.973.10", + "@aws-sdk/token-providers": "3.990.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.8.tgz", + "integrity": "sha512-CZhN1bOc1J3ubQPqbmr5b4KaMJBgdDvYsmEIZuX++wFlzmZsKj1bwkaiTEb5U2V7kXuzLlpF5HJSOM9eY/6nGA==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "@aws-sdk/core": "^3.973.10", + "@aws-sdk/nested-clients": "3.990.0", + "@aws-sdk/types": "^3.973.1", + "@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": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=20.0.0" } }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.27.1", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.3.tgz", + "integrity": "sha512-aknPTb2M+G3s+0qLCx4Li/qGZH8IIYjugHMv15JTYMe6mgZO8VBpYgeGYsNMGCqCZOcWzuf900jFBG5bopfzmA==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@aws-sdk/types": "^3.973.1", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.3.tgz", + "integrity": "sha512-Ftg09xNNRqaz9QNzlfdQWfpqMCJbsQdnZVJP55jfhbKi1+FTWxGuvfPoBhDHIovqWKjqbuiew3HuhxbJ0+OjgA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.3.tgz", + "integrity": "sha512-PY57QhzNuXHnwbJgbWYTrqIDHYSeOlhfYERTAuc16LKZpTZRJUjzBFokp9hF7u1fuGeE3D70ERXzdbMBOqQz7Q==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "@aws-sdk/types": "^3.973.1", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.10.tgz", + "integrity": "sha512-wLkB4bshbBtsAiC2WwlHzOWXu1fx3ftL63fQl0DxEda48Q6B8bcHydZppE3KjEIpPyiNOllByfSnb07cYpIgmw==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@aws-sdk/core": "^3.973.10", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-arn-parser": "^3.972.2", + "@smithy/core": "^3.23.0", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/signature-v4": "^5.3.8", + "@smithy/smithy-client": "^4.11.3", + "@smithy/types": "^4.12.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-stream": "^4.5.12", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-sqs": { + "version": "3.972.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sqs/-/middleware-sdk-sqs-3.972.7.tgz", + "integrity": "sha512-DcJLYE4sRjgUyb2SupQGaRgBYc+j89N9nXeMT0PwwVvaBGmKqcxa7PFvz0kBnQrBckPWlfrPyyyMwOeT5BEp6Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/smithy-client": "^4.11.3", + "@smithy/types": "^4.12.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.27.1", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.10.tgz", + "integrity": "sha512-bBEL8CAqPQkI91ZM5a9xnFAzedpzH6NYCOtNyLarRAzTUTFN2DKqaC60ugBa7pnU1jSi4mA7WAXBsrod7nJltg==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@aws-sdk/core": "^3.973.10", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.990.0", + "@smithy/core": "^3.23.0", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.990.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.990.0.tgz", + "integrity": "sha512-3NA0s66vsy8g7hPh36ZsUgO4SiMyrhwcYvuuNK1PezO52vX3hXDW4pQrC6OQLGKGJV0o6tbEyQtXb/mPs8zg8w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.10", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.10", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.990.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.8", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.23.0", + "@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.14", + "@smithy/middleware-retry": "^4.4.31", + "@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.10", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.3", + "@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.30", + "@smithy/util-defaults-mode-node": "^4.2.33", + "@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/region-config-resolver": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.3.tgz", + "integrity": "sha512-v4J8qYAWfOMcZ4MJUyatntOicTzEMaU7j3OpkRCGGFSL2NgXQ5VbxauIyORA+pxdKZ0qQG2tCQjQjZDlXEC3Ow==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.1", + "@smithy/config-resolver": "^4.4.6", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.990.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.990.0.tgz", + "integrity": "sha512-O55s1eFmKi+2Ko5T1hbdxL6tFVONGscSVe9VRxS4m91Tlbo9iG2Q2HvKWq1DuKQAuUWSUfMmjrRt07JNzizr2A==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "@aws-sdk/middleware-sdk-s3": "^3.972.10", + "@aws-sdk/types": "^3.973.1", + "@smithy/protocol-http": "^5.3.8", + "@smithy/signature-v4": "^5.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/token-providers": { + "version": "3.990.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.990.0.tgz", + "integrity": "sha512-L3BtUb2v9XmYgQdfGBzbBtKMXaP5fV973y3Qdxeevs6oUTVXFmi/mV1+LnScA/1wVPJC9/hlK+1o5vbt7cG7EQ==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@aws-sdk/core": "^3.973.10", + "@aws-sdk/nested-clients": "3.990.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/types": { + "version": "3.973.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.1.tgz", + "integrity": "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.972.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.2.tgz", + "integrity": "sha512-VkykWbqMjlSgBFDyrY3nOSqupMc6ivXuGmvci6Q3NnLq5kC+mKQe2QBZ4nrWRE/jqOxeFP2uYzLtwncYYcvQDg==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.990.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.990.0.tgz", + "integrity": "sha512-kVwtDc9LNI3tQZHEMNbkLIOpeDK8sRSTuT8eMnzGY+O+JImPisfSTjdh+jw9OTznu+MYZjQsv0258sazVKunYg==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.4.tgz", + "integrity": "sha512-H1onv5SkgPBK2P6JR2MjGgbOnttoNzSPIRoeZTNPZYyaplwGg50zS3amXvXqF0/qfXpWEC9rLWU564QTB9bSog==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.3.tgz", + "integrity": "sha512-JurOwkRUcXD/5MTDBcqdyQ9eVedtAsZgw5rBwktsPTN7QtPiS2Ld1jkJepNgYoCufz1Wcut9iup7GJDoIHp8Fw==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.8.tgz", + "integrity": "sha512-XJZuT0LWsFCW1C8dEpPAXSa7h6Pb3krr2y//1X0Zidpcl0vmgY5nL/X0JuBZlntpBzaN3+U4hvKjuijyiiR8zw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "^3.972.10", + "@aws-sdk/types": "^3.973.1", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=20.0.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } } }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.4.tgz", + "integrity": "sha512-0zJ05ANfYqI6+rGqj8samZBFod0dPPousBjLEqg8WdxSgbMAkRgLyn81lP215Do0rFJ/17LIXwr7q0yK24mP6Q==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "@smithy/types": "^4.12.0", + "fast-xml-parser": "5.3.4", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=20.0.0" } }, - "node_modules/@babel/plugin-syntax-typescript": { + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz", + "integrity": "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/code-frame": { "version": "7.27.1", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/template": { - "version": "7.27.2", + "node_modules/@babel/compat-data": { + "version": "7.27.5", "dev": true, "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse": { + "node_modules/@babel/core": { "version": "7.27.4", "dev": true, "license": "MIT", "dependencies": { + "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.4", "@babel/parser": "^7.27.4", "@babel/template": "^7.27.2", + "@babel/traverse": "^7.27.4", "@babel/types": "^7.27.3", - "debug": "^4.3.1", - "globals": "^11.1.0" + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/@babel/types": { - "version": "7.27.6", + "node_modules/@babel/generator": { + "version": "7.27.5", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/parser": "^7.27.5", + "@babel/types": "^7.27.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "dev": true, - "license": "MIT" - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, "engines": { - "node": ">=12" + "node": ">=6.9.0" } }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/@emnapi/core": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", - "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@emnapi/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "tslib": "^2.4.0" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", "dev": true, "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", - "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "aix" - ], "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", - "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ], + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "dev": true, + "license": "MIT", "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", - "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "dev": true, + "license": "MIT", "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", - "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "android" - ], + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "dev": true, + "license": "MIT", "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", - "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], + "node_modules/@babel/helpers": { + "version": "7.27.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", - "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], + "node_modules/@babel/parser": { + "version": "7.27.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, "engines": { - "node": ">=18" + "node": ">=6.0.0" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", - "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", - "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", - "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", - "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", - "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "linux" - ], + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, "engines": { - "node": ">=18" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", - "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", - "cpu": [ - "loong64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", - "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.27.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@datastructures-js/heap": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/@datastructures-js/heap/-/heap-4.3.7.tgz", + "integrity": "sha512-Dx4un7Uj0dVxkfoq4RkpzsY2OrvNJgQYZ3n3UlGdl88RxxdHd7oTi21/l3zoxUUe0sXFuNUrfmWqlHzqnoN6Ug==", + "license": "MIT" + }, + "node_modules/@emnapi/core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", + "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", "cpu": [ - "mips64el" + "ppc64" ], "optional": true, "os": [ - "linux" + "aix" ], "engines": { - "node": ">=18" + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", + "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", + "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", + "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", + "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", + "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", + "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", + "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", + "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", + "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", + "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", + "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", + "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", + "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", + "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", + "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", + "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", + "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", + "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", + "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", + "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", + "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", + "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", + "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", + "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.1.tgz", + "integrity": "sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.3.tgz", + "integrity": "sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.29.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.29.0.tgz", + "integrity": "sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", + "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", - "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ], + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", - "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ], + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", - "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, "engines": { - "node": ">=18" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", - "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/ci-info": { + "version": "3.9.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, "engines": { - "node": ">=18" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", - "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "netbsd" - ], + "node_modules/@jest/globals": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, "engines": { - "node": ">=18" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", - "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "netbsd" - ], + "node_modules/@jest/reporters": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, "engines": { - "node": ">=18" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", - "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "openbsd" - ], + "node_modules/@jest/schemas": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, "engines": { - "node": ">=18" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", - "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "openbsd" - ], + "node_modules/@jest/source-map": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, "engines": { - "node": ">=18" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", - "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "sunos" - ], + "node_modules/@jest/test-result": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, "engines": { - "node": ">=18" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", - "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, "engines": { - "node": ">=18" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", - "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], + "node_modules/@jest/transform": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, "engines": { - "node": ">=18" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", - "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], + "node_modules/@jest/types": { + "version": "29.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, "engines": { - "node": ">=18" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", "dev": true, "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.4.3" + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "node": ">=6.0.0" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", "dev": true, "license": "MIT", "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=6.0.0" } }, - "node_modules/@eslint/config-array": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.1.tgz", - "integrity": "sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } + "license": "MIT" }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", "dev": true, - "license": "ISC", + "license": "MIT", + "optional": true, "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" } }, - "node_modules/@eslint/config-helpers": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.3.tgz", - "integrity": "sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==", + "node_modules/@next/eslint-plugin-next": { + "version": "15.3.4", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.3.4.tgz", + "integrity": "sha512-lBxYdj7TI8phbJcLSAqDt57nIcobEign5NYIKCiy0hXQhrUbTqLqOaSDi568U6vFg4hJfBdZYsG4iP/uKhCqgg==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "fast-glob": "3.3.1" } }, - "node_modules/@eslint/core": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", - "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "node_modules/@next/eslint-plugin-next/node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "peer": true, "dependencies": { - "@types/json-schema": "^7.0.15" + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=8.6.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "node_modules/@next/eslint-plugin-next/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, - "license": "MIT", + "license": "ISC", + "optional": true, + "peer": true, "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" + "is-glob": "^4.0.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">= 6" } }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" } }, - "node_modules/@eslint/eslintrc/node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", "dev": true, "license": "MIT", "engines": { - "node": ">= 4" + "node": ">= 8" } }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" }, "engines": { - "node": "*" + "node": ">= 8" } }, - "node_modules/@eslint/js": { - "version": "9.29.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.29.0.tgz", - "integrity": "sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==", + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@pkgr/core": { + "version": "0.2.7", "dev": true, "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://eslint.org/donate" + "url": "https://opencollective.com/pkgr" } }, - "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "node_modules/@rtsao/scc": { + "version": "1.1.0", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" } }, - "node_modules/@eslint/plugin-kit": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", - "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@smithy/abort-controller": { + "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": { - "@eslint/core": "^0.15.1", - "levn": "^0.4.1" + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18.0.0" } }, - "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", - "dev": true, + "node_modules/@smithy/config-resolver": { + "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": { - "@types/json-schema": "^7.0.15" + "@smithy/node-config-provider": "^4.3.8", + "@smithy/types": "^4.12.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "tslib": "^2.6.2" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=18.0.0" } }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, + "node_modules/@smithy/core": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.0.tgz", + "integrity": "sha512-Yq4UPVoQICM9zHnByLmG8632t2M0+yap4T7ANVw482J0W7HW0pOuxwVmeOwzJqX2Q89fkXz0Vybz55Wj2Xzrsg==", "license": "Apache-2.0", + "dependencies": { + "@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.8", + "@smithy/util-stream": "^4.5.12", + "@smithy/util-utf8": "^4.2.0", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=18.18.0" + "node": ">=18.0.0" } }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, + "node_modules/@smithy/credential-provider-imds": { + "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": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" + "@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": { - "node": ">=18.18.0" + "node": ">=18.0.0" } }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, + "node_modules/@smithy/eventstream-codec": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.8.tgz", + "integrity": "sha512-jS/O5Q14UsufqoGhov7dHLOPCzkYJl9QDzusI2Psh4wyYx/izhzvX9P4D69aTxcdfVhEPhjK+wYyn/PzLjKbbw==", "license": "Apache-2.0", - "engines": { - "node": ">=18.18" + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.12.0", + "@smithy/util-hex-encoding": "^4.2.0", + "tslib": "^2.6.2" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "dev": true, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.8.tgz", + "integrity": "sha512-MTfQT/CRQz5g24ayXdjg53V0mhucZth4PESoA5IhvaWVDTOQLfo8qI9vzqHcPsdd2v6sqfTYqF5L/l+pea5Uyw==", "license": "Apache-2.0", - "engines": { - "node": ">=12.22" + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.8.tgz", + "integrity": "sha512-ah12+luBiDGzBruhu3efNy1IlbwSEdNiw8fOZksoKoWW1ZHvO/04MQsdnws/9Aj+5b0YXSSN2JXKy/ClIsW8MQ==", "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "dependencies": { + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "dev": true, - "license": "ISC", + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.8.tgz", + "integrity": "sha512-cYpCpp29z6EJHa5T9WL0KAlq3SOKUQkcgSoeRfRVwjGgSFl7Uh32eYGt7IDYCX20skiEdRffyDpvF2efEZPC0A==", + "license": "Apache-2.0", "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" + "@smithy/eventstream-serde-universal": "^4.2.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=8" + "node": ">=18.0.0" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { - "version": "1.0.10", - "dev": true, - "license": "MIT", + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.8.tgz", + "integrity": "sha512-iJ6YNJd0bntJYnX6s52NC4WFYcZeKrPUr1Kmmr5AwZcwCSzVpS7oavAmxMR7pMq7V+D1G4s9F5NJK0xwOsKAlQ==", + "license": "Apache-2.0", "dependencies": { - "sprintf-js": "~1.0.2" + "@smithy/eventstream-codec": "^4.2.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { - "version": "4.1.0", - "dev": true, - "license": "MIT", + "node_modules/@smithy/fetch-http-handler": { + "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": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.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" }, "engines": { - "node": ">=8" + "node": ">=18.0.0" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "dev": true, - "license": "MIT", + "node_modules/@smithy/hash-node": { + "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": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "@smithy/types": "^4.12.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { - "version": "5.0.0", - "dev": true, - "license": "MIT", + "node_modules/@smithy/invalid-dependency": { + "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": { - "p-locate": "^4.1.0" + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=8" + "node": ">=18.0.0" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { - "version": "2.3.0", - "dev": true, - "license": "MIT", + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", + "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "license": "Apache-2.0", "dependencies": { - "p-try": "^2.0.0" + "tslib": "^2.6.2" }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18.0.0" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { - "version": "4.1.0", - "dev": true, - "license": "MIT", + "node_modules/@smithy/md5-js": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.8.tgz", + "integrity": "sha512-oGMaLj4tVZzLi3itBa9TCswgMBr7k9b+qKYowQ6x1rTyTuO1IU2YHdHUa+891OsOH+wCsH7aTPRsTJO3RMQmjQ==", + "license": "Apache-2.0", "dependencies": { - "p-limit": "^2.2.0" + "@smithy/types": "^4.12.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=8" + "node": ">=18.0.0" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { - "version": "5.0.0", - "dev": true, - "license": "MIT", + "node_modules/@smithy/middleware-content-length": { + "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.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=8" + "node": ">=18.0.0" } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "dev": true, - "license": "MIT", + "node_modules/@smithy/middleware-endpoint": { + "version": "4.4.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.14.tgz", + "integrity": "sha512-FUFNE5KVeaY6U/GL0nzAAHkaCHzXLZcY1EhtQnsAqhD8Du13oPKtMB9/0WK4/LK6a/T5OZ24wPoSShff5iI6Ag==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.0", + "@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": { - "node": ">=8" + "node": ">=18.0.0" } }, - "node_modules/@jest/console": { - "version": "29.7.0", - "dev": true, - "license": "MIT", + "node_modules/@smithy/middleware-retry": { + "version": "4.4.31", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.31.tgz", + "integrity": "sha512-RXBzLpMkIrxBPe4C8OmEOHvS8aH9RUuCOH++Acb5jZDEblxDjyg6un72X9IcbrGTJoiUwmI7hLypNfuDACypbg==", + "license": "Apache-2.0", "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0" + "@smithy/node-config-provider": "^4.3.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/service-error-classification": "^4.2.8", + "@smithy/smithy-client": "^4.11.3", + "@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" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18.0.0" } }, - "node_modules/@jest/core": { - "version": "29.7.0", - "dev": true, - "license": "MIT", + "node_modules/@smithy/middleware-serde": { + "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": { - "@jest/console": "^29.7.0", - "@jest/reporters": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.7.0", - "jest-config": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-resolve-dependencies": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "jest-watcher": "^29.7.0", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } + "node": ">=18.0.0" } }, - "node_modules/@jest/core/node_modules/ci-info": { - "version": "3.9.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", + "node_modules/@smithy/middleware-stack": { + "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.12.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=8" + "node": ">=18.0.0" } }, - "node_modules/@jest/environment": { - "version": "29.7.0", - "dev": true, - "license": "MIT", + "node_modules/@smithy/node-config-provider": { + "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": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.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": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18.0.0" } }, - "node_modules/@jest/expect": { - "version": "29.7.0", - "dev": true, - "license": "MIT", + "node_modules/@smithy/node-http-handler": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.10.tgz", + "integrity": "sha512-u4YeUwOWRZaHbWaebvrs3UhwQwj+2VNmcVCwXcYTvPIuVyM7Ex1ftAj+fdbG/P4AkBwLq/+SKn+ydOI4ZJE9PA==", + "license": "Apache-2.0", "dependencies": { - "expect": "^29.7.0", - "jest-snapshot": "^29.7.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": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18.0.0" } }, - "node_modules/@jest/expect-utils": { - "version": "29.7.0", - "dev": true, - "license": "MIT", + "node_modules/@smithy/property-provider": { + "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": { - "jest-get-type": "^29.6.3" + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18.0.0" } }, - "node_modules/@jest/fake-timers": { - "version": "29.7.0", - "dev": true, - "license": "MIT", + "node_modules/@smithy/protocol-http": { + "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": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", - "@types/node": "*", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18.0.0" } }, - "node_modules/@jest/globals": { - "version": "29.7.0", - "dev": true, - "license": "MIT", + "node_modules/@smithy/querystring-builder": { + "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": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/types": "^29.6.3", - "jest-mock": "^29.7.0" + "@smithy/types": "^4.12.0", + "@smithy/util-uri-escape": "^4.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18.0.0" } }, - "node_modules/@jest/reporters": { - "version": "29.7.0", - "dev": true, - "license": "MIT", + "node_modules/@smithy/querystring-parser": { + "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": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", - "v8-to-istanbul": "^9.0.1" + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "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.12.0" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "dev": true, - "license": "MIT", + "node_modules/@smithy/shared-ini-file-loader": { + "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": { - "@sinclair/typebox": "^0.27.8" + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18.0.0" } }, - "node_modules/@jest/source-map": { - "version": "29.6.3", - "dev": true, - "license": "MIT", + "node_modules/@smithy/signature-v4": { + "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": { - "@jridgewell/trace-mapping": "^0.3.18", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-uri-escape": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18.0.0" } }, - "node_modules/@jest/test-result": { - "version": "29.7.0", - "dev": true, - "license": "MIT", + "node_modules/@smithy/smithy-client": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.11.3.tgz", + "integrity": "sha512-Q7kY5sDau8OoE6Y9zJoRGgje8P4/UY0WzH8R2ok0PDh+iJ+ZnEKowhjEqYafVcubkbYxQVaqwm3iufktzhprGg==", + "license": "Apache-2.0", "dependencies": { - "@jest/console": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" + "@smithy/core": "^3.23.0", + "@smithy/middleware-endpoint": "^4.4.14", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "@smithy/util-stream": "^4.5.12", + "tslib": "^2.6.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18.0.0" } }, - "node_modules/@jest/test-sequencer": { - "version": "29.7.0", - "dev": true, - "license": "MIT", + "node_modules/@smithy/types": { + "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": { - "@jest/test-result": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "slash": "^3.0.0" + "tslib": "^2.6.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18.0.0" } }, - "node_modules/@jest/transform": { - "version": "29.7.0", - "dev": true, - "license": "MIT", + "node_modules/@smithy/url-parser": { + "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": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" + "@smithy/querystring-parser": "^4.2.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18.0.0" } }, - "node_modules/@jest/types": { - "version": "29.6.3", - "dev": true, - "license": "MIT", + "node_modules/@smithy/util-base64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", + "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", + "license": "Apache-2.0", "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18.0.0" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "dev": true, - "license": "MIT", + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", + "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", + "license": "Apache-2.0", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.0.0" + "node": ">=18.0.0" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "dev": true, - "license": "MIT", + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", + "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.0.0" + "node": ">=18.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "dev": true, - "license": "MIT", + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", + "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.0.0" + "node": ">=18.0.0" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "dev": true, - "license": "MIT", + "node_modules/@smithy/util-config-provider": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", + "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", + "license": "Apache-2.0", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", - "dev": true, - "license": "MIT", - "optional": true, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.30", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.30.tgz", + "integrity": "sha512-cMni0uVU27zxOiU8TuC8pQLC1pYeZ/xEMxvchSK/ILwleRd1ugobOcIRr5vXtcRqKd4aBLWlpeBoDPJJ91LQng==", + "license": "Apache-2.0", "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" + "@smithy/property-provider": "^4.2.8", + "@smithy/smithy-client": "^4.11.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@next/eslint-plugin-next": { - "version": "15.3.4", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.3.4.tgz", - "integrity": "sha512-lBxYdj7TI8phbJcLSAqDt57nIcobEign5NYIKCiy0hXQhrUbTqLqOaSDi568U6vFg4hJfBdZYsG4iP/uKhCqgg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.33", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.33.tgz", + "integrity": "sha512-LEb2aq5F4oZUSzWBG7S53d4UytZSkOEJPXcBq/xbG2/TmK9EW5naUZ8lKu1BEyWMzdHIzEVN16M3k8oxDq+DJA==", + "license": "Apache-2.0", "dependencies": { - "fast-glob": "3.3.1" + "@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.11.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@next/eslint-plugin-next/node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, + "node_modules/@smithy/util-endpoints": { + "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": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "@smithy/node-config-provider": "^4.3.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=8.6.0" + "node": ">=18.0.0" } }, - "node_modules/@next/eslint-plugin-next/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", + "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", + "license": "Apache-2.0", "dependencies": { - "is-glob": "^4.0.1" + "tslib": "^2.6.2" }, "engines": { - "node": ">= 6" + "node": ">=18.0.0" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "dev": true, - "license": "MIT", + "node_modules/@smithy/util-middleware": { + "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": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">= 8" + "node": ">=18.0.0" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "dev": true, - "license": "MIT", + "node_modules/@smithy/util-retry": { + "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.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">= 8" + "node": ">=18.0.0" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "license": "MIT", + "node_modules/@smithy/util-stream": { + "version": "4.5.12", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.12.tgz", + "integrity": "sha512-D8tgkrmhAX/UNeCZbqbEO3uqyghUnEmmoO9YEvRuwxjlkKKUE7FOgCJnqpTlQPe9MApdWPky58mNQQHbnCzoNg==", + "license": "Apache-2.0", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/node-http-handler": "^4.4.10", + "@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", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">= 8" + "node": ">=18.0.0" } }, - "node_modules/@pkgr/core": { - "version": "0.2.7", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", + "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" }, - "funding": { - "url": "https://opencollective.com/pkgr" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "dev": true, - "license": "MIT" - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "dev": true, - "license": "BSD-3-Clause", + "node_modules/@smithy/util-utf8": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", + "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "license": "Apache-2.0", "dependencies": { - "type-detect": "4.0.8" + "@smithy/util-buffer-from": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "dev": true, - "license": "BSD-3-Clause", + "node_modules/@smithy/uuid": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", + "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", + "license": "Apache-2.0", "dependencies": { - "@sinonjs/commons": "^3.0.0" + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, "node_modules/@stylistic/eslint-plugin": { @@ -2580,6 +4181,28 @@ "node": ">= 6.0.0" } }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aggregate-error/node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2597,6 +4220,45 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/ansi-escapes": { "version": "4.3.2", "dev": true, @@ -2850,14 +4512,36 @@ "node": ">= 0.4" } }, + "node_modules/async-wait-until": { + "version": "2.0.31", + "resolved": "https://registry.npmjs.org/async-wait-until/-/async-wait-until-2.0.31.tgz", + "integrity": "sha512-9VCfHvc4f36oT6sG5p16aKc9zojf3wF4FrjNDxU3Db51SJ1bQ5lWAWtQDDZPysTwSLKBDzNZ083qPkTIj6XnrA==", + "license": "MIT", + "engines": { + "node": ">= 0.14.0", + "npm": ">= 1.0.0" + }, + "funding": { + "type": "individual", + "url": "http://paypal.me/devlatoau" + } + }, "node_modules/asynckit": { "version": "0.4.0", "dev": true, "license": "MIT" }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", - "dev": true, "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" @@ -2869,6 +4553,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/aws-embedded-metrics": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/aws-embedded-metrics/-/aws-embedded-metrics-4.2.1.tgz", + "integrity": "sha512-uzydBXlGQVTB2sZ9ACCQZM3y0u4wdvxxRKFL9LP6RdfI2GcOrCcAsz65UKQvX9iagxFhah322VvvatgP8E7MIg==", + "license": "Apache-2.0", + "dependencies": { + "@datastructures-js/heap": "^4.0.2" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/axe-core": { "version": "4.10.3", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", @@ -3006,6 +4702,21 @@ "dev": true, "license": "MIT" }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "2.0.2", "dev": true, @@ -3103,7 +4814,6 @@ }, "node_modules/call-bind": { "version": "1.0.8", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.0", @@ -3120,7 +4830,6 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -3132,7 +4841,6 @@ }, "node_modules/call-bound": { "version": "1.0.4", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -3241,6 +4949,15 @@ "node": ">=0.8.0" } }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/cliui": { "version": "8.0.1", "dev": true, @@ -3254,6 +4971,45 @@ "node": ">=12" } }, + "node_modules/cloudevents": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/cloudevents/-/cloudevents-8.0.3.tgz", + "integrity": "sha512-wTixKNjfLeyj9HQpESvLVVO4xgdqdvX4dTeg1IZ2SCunu/fxVzCamcIZneEyj31V82YolFCKwVeSkr8zResB0Q==", + "license": "Apache-2.0", + "dependencies": { + "ajv": "^8.11.0", + "ajv-formats": "^2.1.1", + "json-bigint": "^1.0.0", + "process": "^0.11.10", + "util": "^0.12.4", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=16 <=22" + } + }, + "node_modules/cloudevents/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/cloudevents/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/co": { "version": "4.6.0", "dev": true, @@ -3520,7 +5276,6 @@ }, "node_modules/define-data-property": { "version": "1.1.4", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -3670,7 +5425,6 @@ }, "node_modules/dunder-proto": { "version": "1.0.1", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -3806,7 +5560,6 @@ }, "node_modules/es-define-property": { "version": "1.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3814,7 +5567,6 @@ }, "node_modules/es-errors": { "version": "1.3.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3850,7 +5602,6 @@ }, "node_modules/es-object-atoms": { "version": "1.1.1", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -4919,7 +6670,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-diff": { @@ -4963,6 +6713,40 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-xml-parser": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.6.tgz", + "integrity": "sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.2" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.19.1", "dev": true, @@ -5077,7 +6861,6 @@ }, "node_modules/for-each": { "version": "0.3.5", - "dev": true, "license": "MIT", "dependencies": { "is-callable": "^1.2.7" @@ -5128,7 +6911,6 @@ }, "node_modules/function-bind": { "version": "1.1.2", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5184,7 +6966,6 @@ }, "node_modules/get-intrinsic": { "version": "1.3.0", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -5215,7 +6996,6 @@ }, "node_modules/get-proto": { "version": "1.0.1", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -5343,7 +7123,6 @@ }, "node_modules/gopd": { "version": "1.2.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5378,7 +7157,6 @@ }, "node_modules/has-property-descriptors": { "version": "1.0.2", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" @@ -5403,7 +7181,6 @@ }, "node_modules/has-symbols": { "version": "1.1.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5414,7 +7191,6 @@ }, "node_modules/has-tostringtag": { "version": "1.0.2", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -5428,7 +7204,6 @@ }, "node_modules/hasown": { "version": "2.0.2", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -5590,7 +7365,6 @@ }, "node_modules/inherits": { "version": "2.0.4", - "dev": true, "license": "ISC" }, "node_modules/internal-slot": { @@ -5606,6 +7380,22 @@ "node": ">= 0.4" } }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "dev": true, @@ -5713,7 +7503,6 @@ }, "node_modules/is-callable": { "version": "1.2.7", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5807,7 +7596,6 @@ }, "node_modules/is-generator-function": { "version": "1.1.0", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -5885,7 +7673,6 @@ }, "node_modules/is-regex": { "version": "1.2.1", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -5969,7 +7756,6 @@ }, "node_modules/is-typed-array": { "version": "1.1.15", - "dev": true, "license": "MIT", "dependencies": { "which-typed-array": "^1.1.16" @@ -6839,6 +8625,15 @@ "node": ">=6" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -7091,7 +8886,6 @@ }, "node_modules/math-intrinsics": { "version": "1.1.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7225,10 +9019,18 @@ "dev": true, "license": "MIT" }, + "node_modules/nhs-notify-client-callbacks-integration-tests": { + "resolved": "tests/integration", + "link": true + }, "node_modules/nhs-notify-client-transform-filter-lambda": { "resolved": "lambdas/client-transform-filter-lambda", "link": true }, + "node_modules/nhs-notify-mock-webhook-lambda": { + "resolved": "lambdas/mock-webhook-lambda", + "link": true + }, "node_modules/node-int64": { "version": "0.4.0", "dev": true, @@ -7376,6 +9178,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/once": { "version": "1.4.0", "dev": true, @@ -7552,6 +9363,43 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "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" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, "node_modules/pirates": { "version": "4.0.7", "dev": true, @@ -7629,7 +9477,6 @@ }, "node_modules/possible-typed-array-names": { "version": "1.1.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7693,6 +9540,31 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/prompts": { "version": "2.4.2", "dev": true, @@ -7782,11 +9654,26 @@ ], "license": "MIT" }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, "node_modules/react-is": { "version": "19.0.0", "dev": true, "license": "MIT" }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/refa": { "version": "0.12.1", "dev": true, @@ -7892,6 +9779,15 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/requires-port": { "version": "1.0.0", "dev": true, @@ -8035,7 +9931,6 @@ }, "node_modules/safe-regex-test": { "version": "1.1.0", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -8049,6 +9944,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "dev": true, @@ -8091,7 +9995,6 @@ }, "node_modules/set-function-length": { "version": "1.2.2", - "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -8237,6 +10140,15 @@ "node": ">=8" } }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/source-map": { "version": "0.6.1", "dev": true, @@ -8254,6 +10166,15 @@ "source-map": "^0.6.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "dev": true, @@ -8486,6 +10407,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", + "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "dev": true, @@ -8560,6 +10493,15 @@ "node": "*" } }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -8823,9 +10765,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true + "license": "0BSD" }, "node_modules/tsx": { "version": "4.20.3", @@ -9519,6 +11459,28 @@ "requires-port": "^1.0.0" } }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "dev": true, @@ -9714,7 +11676,6 @@ }, "node_modules/which-typed-array": { "version": "1.1.19", - "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", @@ -9870,6 +11831,22 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "tests/integration": { + "name": "nhs-notify-client-callbacks-integration-tests", + "version": "0.0.1", + "dependencies": { + "@aws-sdk/client-cloudwatch-logs": "^3.991.0", + "@aws-sdk/client-eventbridge": "^3.990.0", + "@aws-sdk/client-sqs": "^3.990.0", + "async-wait-until": "^2.0.12" + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.2", + "@types/jest": "^29.5.14", + "jest": "^29.7.0", + "typescript": "^5.8.2" + } } } } diff --git a/package.json b/package.json index 2013466..a3afaac 100644 --- a/package.json +++ b/package.json @@ -36,17 +36,21 @@ "pretty-format": { "react-is": "19.0.0" }, - "react": "^19.0.0" + "react": "^19.0.0", + "fast-xml-parser": "^5.3.6" }, "scripts": { "generate-dependencies": "npm run generate-dependencies --workspaces --if-present", "lint": "npm run lint --workspaces", "lint:fix": "npm run lint:fix --workspaces", "start": "npm run start --workspace frontend", + "test:integration": "npm run test:integration --workspace tests/integration", "test:unit": "npm run test:unit --workspaces", "typecheck": "npm run typecheck --workspaces" }, "workspaces": [ - "lambdas/client-transform-filter-lambda" + "lambdas/client-transform-filter-lambda", + "lambdas/mock-webhook-lambda", + "tests/integration" ] } diff --git a/scripts/config/sonar-scanner.properties b/scripts/config/sonar-scanner.properties index 9fbc7fc..6543678 100644 --- a/scripts/config/sonar-scanner.properties +++ b/scripts/config/sonar-scanner.properties @@ -3,12 +3,7 @@ sonar.host.url=https://sonarcloud.io sonar.qualitygate.wait=true sonar.sourceEncoding=UTF-8 -sonar.sources=lambdas/example-lambda -sonar.tests=tests/, lambdas/example-lambda/src/__tests__ -sonar.exclusions=lambdas/*/src/__tests__/**/* sonar.terraform.provider.aws.version=5.54.1 sonar.cpd.exclusions=**.test.* -sonar.coverage.exclusions=tests/, **/*.dev.*, lambdas/**/src/__tests__, utils/utils/src/zod-validators.ts ,**/jest.config.ts,scripts/**/* - -#sonar.python.coverage.reportPaths=.coverage/coverage.xml +sonar.coverage.exclusions=tests/**, lambdas/**/src/__tests__/**, **/jest.config.ts sonar.javascript.lcov.reportPaths=lcov.info diff --git a/scripts/config/vale/styles/config/vocabularies/words/accept.txt b/scripts/config/vale/styles/config/vocabularies/words/accept.txt index e8c5758..26f8ed1 100644 --- a/scripts/config/vale/styles/config/vocabularies/words/accept.txt +++ b/scripts/config/vale/styles/config/vocabularies/words/accept.txt @@ -22,6 +22,7 @@ Octokit onboarding Podman Python +queryable rawContent [Rr]unbook sed diff --git a/scripts/tests/integration.sh b/scripts/tests/integration.sh new file mode 100755 index 0000000..8460d02 --- /dev/null +++ b/scripts/tests/integration.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -euo pipefail + +cd "$(git rev-parse --show-toplevel)" + +npm ci +npm run test:integration diff --git a/scripts/tests/unit.sh b/scripts/tests/unit.sh index 8b3021f..c8282ba 100755 --- a/scripts/tests/unit.sh +++ b/scripts/tests/unit.sh @@ -19,7 +19,7 @@ cd "$(git rev-parse --show-toplevel)" # run tests npm ci -npm run test:unit --workspaces +npm run test:unit # merge coverage reports mkdir -p .reports diff --git a/tests/integration/event-bus-to-webhook.test.ts b/tests/integration/event-bus-to-webhook.test.ts new file mode 100644 index 0000000..4fc6aae --- /dev/null +++ b/tests/integration/event-bus-to-webhook.test.ts @@ -0,0 +1,318 @@ +import { + EventBridgeClient, + PutEventsCommand, +} from "@aws-sdk/client-eventbridge"; +import type { PutEventsRequestEntry } from "@aws-sdk/client-eventbridge"; +import { + GetQueueAttributesCommand, + PurgeQueueCommand, + SQSClient, +} from "@aws-sdk/client-sqs"; +import { waitUntil } from "async-wait-until"; +import type { StatusTransitionEvent } from "nhs-notify-client-transform-filter-lambda/src/models/status-transition-event"; +import type { MessageStatusData } from "nhs-notify-client-transform-filter-lambda/src/models/message-status-data"; + +const publishEvent = async ( + client: EventBridgeClient, + eventBusName: string, + event: StatusTransitionEvent, +) => { + const putEventsCommand = new PutEventsCommand({ + Entries: [ + { + EventBusName: eventBusName, + Source: event.source, + DetailType: event.type, + Detail: JSON.stringify(event), + Time: new Date(event.time), + } as PutEventsRequestEntry, + ], + }); + + return client.send(putEventsCommand); +}; + +const getQueueMessageCount = async ( + client: SQSClient, + queueUrl?: string, + attributeNames: ( + | "ApproximateNumberOfMessages" + | "ApproximateNumberOfMessagesNotVisible" + )[] = ["ApproximateNumberOfMessages"], +) => { + if (!queueUrl) { + return 0; + } + + const queueAttributesCommand = new GetQueueAttributesCommand({ + QueueUrl: queueUrl, + AttributeNames: attributeNames, + }); + + const queueAttributes = await client.send(queueAttributesCommand); + + return Number(queueAttributes.Attributes?.ApproximateNumberOfMessages || 0); +}; + +const awaitQueueEmpty = async ( + client: SQSClient, + queueUrl?: string, + attributeNames: ( + | "ApproximateNumberOfMessages" + | "ApproximateNumberOfMessagesNotVisible" + )[] = ["ApproximateNumberOfMessages"], +) => { + if (!queueUrl) { + return; + } + + await waitUntil( + async () => + (await getQueueMessageCount(client, queueUrl, attributeNames)) === 0, + { + intervalBetweenAttempts: 250, + timeout: 10_000, + }, + ); +}; + +const awaitMessageStatusCallbacks = async ( + logGroup: string, + messageId: string, +) => { + const { getMessageStatusCallbacks } = await import("./helpers/index.js"); + let callbacks: Awaited> = []; + + await waitUntil( + async () => { + callbacks = await getMessageStatusCallbacks(logGroup, messageId); + return callbacks.length > 0; + }, + { + intervalBetweenAttempts: 500, + timeout: 10_000, + }, + ); + + if (callbacks.length === 0) { + throw new Error("Timed out waiting for message status callbacks"); + } + + return callbacks; +}; + +// eslint-disable-next-line jest/no-disabled-tests +describe.skip("Event Bus to Webhook Integration", () => { + let eventBridgeClient: EventBridgeClient; + let sqsClient: SQSClient; + + const TEST_EVENT_BUS_NAME = + process.env.TEST_EVENT_BUS_NAME || "nhs-notify-shared-event-bus-dev"; + const { TEST_QUEUE_URL } = process.env; + const { TEST_WEBHOOK_URL } = process.env; + const { TEST_WEBHOOK_LOG_GROUP } = process.env; + + beforeAll(() => { + eventBridgeClient = new EventBridgeClient({ region: "eu-west-2" }); + sqsClient = new SQSClient({ region: "eu-west-2" }); + }); + + afterAll(() => { + eventBridgeClient.destroy(); + sqsClient.destroy(); + }); + + beforeEach(async () => { + if (TEST_QUEUE_URL) { + try { + await sqsClient.send( + new PurgeQueueCommand({ + QueueUrl: TEST_QUEUE_URL, + }), + ); + } catch (error) { + if (error instanceof Error && error.name !== "PurgeQueueInProgress") { + throw error; + } + } + } + }); + + describe("Message Status Event Flow", () => { + it("should process message status event from Event Bus to webhook", async () => { + if (!TEST_WEBHOOK_URL) { + return; + } + + if (!TEST_WEBHOOK_LOG_GROUP) { + throw new Error("TEST_WEBHOOK_LOG_GROUP must be set for this test"); + } + + const messageStatusEvent: StatusTransitionEvent = { + specversion: "1.0", + id: crypto.randomUUID(), + source: + "/nhs/england/notify/development/primary/data-plane/client-callbacks", + subject: `customer/${crypto.randomUUID()}/message/test-msg-${Date.now()}`, + type: "uk.nhs.notify.client-callbacks.message.status.transitioned.v1", + time: new Date().toISOString(), + datacontenttype: "application/json", + dataschema: "https://nhs.uk/schemas/notify/message-status-data.v1.json", + traceparent: "00-4d678967f96e353c07a0a31c1849b500-07f83ba58dd8df70-01", + data: { + clientId: "test-client", + messageId: `test-msg-${Date.now()}`, + messageReference: `test-ref-${Date.now()}`, + messageStatus: "DELIVERED", + messageStatusDescription: "Integration test message delivered", + channels: [ + { + type: "NHSAPP", + channelStatus: "DELIVERED", + }, + ], + timestamp: new Date().toISOString(), + routingPlan: { + id: `routing-plan-${crypto.randomUUID()}`, + name: "Test routing plan", + version: "v1.0.0", + createdDate: new Date().toISOString(), + }, + }, + }; + + const putEventsResponse = await publishEvent( + eventBridgeClient, + TEST_EVENT_BUS_NAME, + messageStatusEvent, + ); + + expect(putEventsResponse.FailedEntryCount).toBe(0); + expect(putEventsResponse.Entries).toHaveLength(1); + expect(putEventsResponse.Entries![0].EventId).toBeDefined(); + + await awaitQueueEmpty(sqsClient, TEST_QUEUE_URL, [ + "ApproximateNumberOfMessages", + "ApproximateNumberOfMessagesNotVisible", + ]); + + const callbacks = await awaitMessageStatusCallbacks( + TEST_WEBHOOK_LOG_GROUP, + messageStatusEvent.data.messageId, + ); + + expect(callbacks).toHaveLength(1); + + expect(callbacks[0]).toMatchObject({ + type: "MessageStatus", + + attributes: expect.objectContaining({ + messageStatus: "delivered", + }), + }); + }, 30_000); + + it("should filter out events not matching client subscription", async () => { + if (!TEST_WEBHOOK_URL) { + return; + } + + const messageStatusEvent: StatusTransitionEvent = { + specversion: "1.0", + id: crypto.randomUUID(), + source: + "/nhs/england/notify/development/primary/data-plane/client-callbacks", + subject: `customer/${crypto.randomUUID()}/message/test-msg-${Date.now()}`, + type: "uk.nhs.notify.client-callbacks.message.status.transitioned.v1", + time: new Date().toISOString(), + datacontenttype: "application/json", + dataschema: "https://nhs.uk/schemas/notify/message-status-data.v1.json", + traceparent: "00-4d678967f96e353c07a0a31c1849b500-07f83ba58dd8df70-01", + data: { + clientId: "non-existent-client", // Client not in subscription config + messageId: `test-msg-${Date.now()}`, + messageReference: `test-ref-${Date.now()}`, + messageStatus: "DELIVERED", + channels: [ + { + type: "NHSAPP", + channelStatus: "DELIVERED", + }, + ], + timestamp: new Date().toISOString(), + routingPlan: { + id: `routing-plan-${crypto.randomUUID()}`, + name: "Test routing plan", + version: "v1.0.0", + createdDate: new Date().toISOString(), + }, + }, + }; + + const putEventsResponse = await publishEvent( + eventBridgeClient, + TEST_EVENT_BUS_NAME, + messageStatusEvent, + ); + + expect(putEventsResponse.FailedEntryCount).toBe(0); + + await awaitQueueEmpty(sqsClient, TEST_QUEUE_URL, [ + "ApproximateNumberOfMessages", + "ApproximateNumberOfMessagesNotVisible", + ]); + }, 30_000); + }); + + describe("Channel Status Event Flow", () => { + it("should process channel status event from Event Bus to webhook", async () => { + if (!TEST_WEBHOOK_URL) { + return; + } + + const channelStatusEvent: StatusTransitionEvent = { + specversion: "1.0", + id: crypto.randomUUID(), + source: + "/nhs/england/notify/development/primary/data-plane/client-callbacks", + subject: `customer/${crypto.randomUUID()}/message/test-msg-${Date.now()}/channel/nhsapp`, + type: "uk.nhs.notify.client-callbacks.channel.status.transitioned.v1", + time: new Date().toISOString(), + datacontenttype: "application/json", + dataschema: "https://nhs.uk/schemas/notify/channel-status-data.v1.json", + traceparent: "00-4d678967f96e353c07a0a31c1849b500-07f83ba58dd8df70-02", + data: { + clientId: "test-client", + messageId: `test-msg-${Date.now()}`, + messageReference: `test-ref-${Date.now()}`, + channel: "NHSAPP", + channelStatus: "DELIVERED", + channelStatusDescription: "Integration test channel delivered", + supplierStatus: "DELIVERED", + cascadeType: "primary", + cascadeOrder: 1, + timestamp: new Date().toISOString(), + retryCount: 0, + routingPlan: { + id: `routing-plan-${crypto.randomUUID()}`, + name: "Test routing plan", + version: "v1.0.0", + createdDate: new Date().toISOString(), + }, + }, + }; + + const putEventsResponse = await publishEvent( + eventBridgeClient, + TEST_EVENT_BUS_NAME, + channelStatusEvent, + ); + + expect(putEventsResponse.FailedEntryCount).toBe(0); + expect(putEventsResponse.Entries).toHaveLength(1); + expect(putEventsResponse.Entries![0].EventId).toBeDefined(); + + await awaitQueueEmpty(sqsClient, TEST_QUEUE_URL); + }, 30_000); + }); +}); diff --git a/tests/integration/helpers/cloudwatch-helpers.ts b/tests/integration/helpers/cloudwatch-helpers.ts new file mode 100644 index 0000000..7928205 --- /dev/null +++ b/tests/integration/helpers/cloudwatch-helpers.ts @@ -0,0 +1,98 @@ +import { + CloudWatchLogsClient, + FilterLogEventsCommand, +} from "@aws-sdk/client-cloudwatch-logs"; +import type { CallbackPayload } from "nhs-notify-mock-webhook-lambda/src/types"; + +const client = new CloudWatchLogsClient({ region: "eu-west-2" }); + +export async function getCallbackLogsFromCloudWatch( + logGroupName: string, + pattern: string, + startTime?: Date, +): Promise { + const searchStartTime = startTime || new Date(Date.now() - 5 * 60 * 1000); + + const filterEvents = new FilterLogEventsCommand({ + logGroupName, + startTime: searchStartTime.getTime(), + filterPattern: pattern, + limit: 100, + }); + + const { events = [] } = await client.send(filterEvents); + + return events.flatMap(({ message }) => + message ? [JSON.parse(message)] : [], + ); +} + +export function parseCallbacksFromLogs(logs: unknown[]): CallbackPayload[] { + return logs + .map((log: unknown) => { + if ( + typeof log === "object" && + log !== null && + "msg" in log && + typeof log.msg === "string" + ) { + const match = /CALLBACK .+ : (.+)$/.exec(log.msg); + if (match?.[1]) { + try { + return JSON.parse(match[1]) as CallbackPayload; + } catch { + return null; + } + } + } + return null; + }) + .filter((payload): payload is CallbackPayload => payload !== null); +} + +export async function getMessageStatusCallbacks( + logGroupName: string, + requestItemId: string, + startTime?: Date, +): Promise { + const logs = await getCallbackLogsFromCloudWatch( + logGroupName, + `%${requestItemId}%MessageStatus%`, + startTime, + ); + return parseCallbacksFromLogs(logs); +} + +export async function getChannelStatusCallbacks( + logGroupName: string, + requestItemId: string, + startTime?: Date, +): Promise { + const logs = await getCallbackLogsFromCloudWatch( + logGroupName, + `%${requestItemId}%ChannelStatus%`, + startTime, + ); + return parseCallbacksFromLogs(logs); +} + +/** + * Get all callbacks for a specific message ID + * + * @param logGroupName - CloudWatch log group name + * @param requestItemId - Message ID to filter by + * @param startTime - Optional start time for search + * @returns Array of all callback payloads (MessageStatus and ChannelStatus) + */ +export async function getAllCallbacks( + logGroupName: string, + requestItemId: string, + startTime?: Date, +): Promise { + const logs = await getCallbackLogsFromCloudWatch( + logGroupName, + `"${requestItemId}"`, + startTime, + ); + return parseCallbacksFromLogs(logs); +} diff --git a/tests/integration/helpers/index.ts b/tests/integration/helpers/index.ts new file mode 100644 index 0000000..b0718c3 --- /dev/null +++ b/tests/integration/helpers/index.ts @@ -0,0 +1 @@ +export * from "./cloudwatch-helpers"; diff --git a/tests/integration/jest.config.ts b/tests/integration/jest.config.ts new file mode 100644 index 0000000..6029330 --- /dev/null +++ b/tests/integration/jest.config.ts @@ -0,0 +1,5 @@ +import { nodeJestConfig } from "../../jest.config.base"; + +export default { + ...nodeJestConfig, +}; diff --git a/tests/integration/package.json b/tests/integration/package.json new file mode 100644 index 0000000..62e7cfb --- /dev/null +++ b/tests/integration/package.json @@ -0,0 +1,24 @@ +{ + "name": "nhs-notify-client-callbacks-integration-tests", + "version": "0.0.1", + "private": true, + "scripts": { + "test:integration": "jest", + "test:unit": "echo 'No unit tests in integration workspace - skipping'", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@aws-sdk/client-cloudwatch-logs": "^3.991.0", + "@aws-sdk/client-eventbridge": "^3.990.0", + "@aws-sdk/client-sqs": "^3.990.0", + "async-wait-until": "^2.0.12" + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.2", + "@types/jest": "^29.5.14", + "jest": "^29.7.0", + "typescript": "^5.8.2" + } +} diff --git a/tests/integration/tsconfig.json b/tests/integration/tsconfig.json new file mode 100644 index 0000000..3fcd6a2 --- /dev/null +++ b/tests/integration/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "isolatedModules": true, + "paths": { + "models/*": [ + "../../lambdas/client-transform-filter-lambda/src/models/*" + ], + "services/*": [ + "../../lambdas/client-transform-filter-lambda/src/services/*" + ], + "transformers/*": [ + "../../lambdas/client-transform-filter-lambda/src/transformers/*" + ], + "validators/*": [ + "../../lambdas/client-transform-filter-lambda/src/validators/*" + ] + } + }, + "extends": "../../tsconfig.base.json", + "include": [ + "**/*.ts", + "jest.config.ts" + ] +} diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index ccb61df..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "extends": "./tsconfig.base.json", - "compilerOptions": { - "noEmit": true - }, - "include": [ - "lambdas/*/src/**/*", - "scripts/*/src/**/*", - "src/**/*", - "tests/**/*" - ] -}