diff --git a/infrastructure/terraform/components/callbacks/README.md b/infrastructure/terraform/components/callbacks/README.md index d69eaeb..fdb893e 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` | `false` | 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 | @@ -32,13 +33,20 @@ | 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\_function\_arn](#output\_mock\_webhook\_lambda\_function\_arn) | ARN of the mock webhook lambda function (only present when deploy\_mock\_webhook=true) | +| [mock\_webhook\_lambda\_function\_name](#output\_mock\_webhook\_lambda\_function\_name) | Name of the mock webhook lambda function (only present when deploy\_mock\_webhook=true) | +| [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/module_mock_webhook_lambda.tf b/infrastructure/terraform/components/callbacks/module_mock_webhook_lambda.tf new file mode 100644 index 0000000..6d444ef --- /dev/null +++ b/infrastructure/terraform/components/callbacks/module_mock_webhook_lambda.tf @@ -0,0 +1,76 @@ +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 + } +} diff --git a/infrastructure/terraform/components/callbacks/module_transform_filter_lambda.tf b/infrastructure/terraform/components/callbacks/module_transform_filter_lambda.tf index c0ce5da..ebbb3b0 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"] @@ -38,7 +38,7 @@ module "client_transform_filter_lambda" { } } -data "aws_iam_policy_document" "example_lambda" { +data "aws_iam_policy_document" "client_transform_filter_lambda" { statement { sid = "KMSPermissions" effect = "Allow" @@ -52,4 +52,17 @@ 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}/*", + ] + } } diff --git a/infrastructure/terraform/components/callbacks/outputs.tf b/infrastructure/terraform/components/callbacks/outputs.tf index 9dcc2f3..9a67b32 100644 --- a/infrastructure/terraform/components/callbacks/outputs.tf +++ b/infrastructure/terraform/components/callbacks/outputs.tf @@ -1 +1,25 @@ # 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_function_name" { + description = "Name of the mock webhook lambda function (only present when deploy_mock_webhook=true)" + value = var.deploy_mock_webhook ? module.mock_webhook_lambda[0].function_name : null +} + +output "mock_webhook_lambda_function_arn" { + description = "ARN of the mock webhook lambda function (only present when deploy_mock_webhook=true)" + value = var.deploy_mock_webhook ? module.mock_webhook_lambda[0].function_arn : null +} + +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/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..8d8e2d5 100644 --- a/infrastructure/terraform/components/callbacks/variables.tf +++ b/infrastructure/terraform/components/callbacks/variables.tf @@ -112,3 +112,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 = false +} diff --git a/lambdas/client-transform-filter-lambda/jest.config.ts b/lambdas/client-transform-filter-lambda/jest.config.ts index f88e727..4cec36d 100644 --- a/lambdas/client-transform-filter-lambda/jest.config.ts +++ b/lambdas/client-transform-filter-lambda/jest.config.ts @@ -55,6 +55,9 @@ const utilsJestConfig = { ...(baseJestConfig.coveragePathIgnorePatterns ?? []), "zod-validators.ts", ], + + // Mirror tsconfig's baseUrl: "src" - automatically resolves non-relative imports + modulePaths: ["/src"], }; export default utilsJestConfig; diff --git a/lambdas/client-transform-filter-lambda/package.json b/lambdas/client-transform-filter-lambda/package.json index f288265..39f107a 100644 --- a/lambdas/client-transform-filter-lambda/package.json +++ b/lambdas/client-transform-filter-lambda/package.json @@ -1,6 +1,9 @@ { "dependencies": { - "esbuild": "^0.25.0" + "@aws-sdk/client-cloudwatch": "^3.709.0", + "cloudevents": "^8.0.2", + "esbuild": "^0.25.0", + "pino": "^9.5.0" }, "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..b1dd0d3 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,121 @@ +import type { StatusTransitionEvent } from "models/status-transition-event"; +import type { MessageStatusData } from "models/message-status-data"; import { handler } from ".."; +// Mock the metrics service to avoid actual CloudWatch calls +jest.mock("services/metrics", () => ({ + metricsService: { + emitEventReceived: jest.fn().mockImplementation(async () => {}), + emitTransformationSuccess: jest.fn().mockImplementation(async () => {}), + emitDeliveryInitiated: jest.fn().mockImplementation(async () => {}), + emitValidationError: jest.fn().mockImplementation(async () => {}), + emitTransformationFailure: jest.fn().mockImplementation(async () => {}), + emitProcessingLatency: jest.fn().mockImplementation(async () => {}), + }, +})); + 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 validMessageStatusEvent: StatusTransitionEvent = { + profileversion: "1.0.0", + profilepublished: "2025-10", + 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", + recordedtime: "2026-02-05T14:30:00.150Z", + datacontenttype: "application/json", + dataschema: "https://nhs.uk/schemas/notify/message-status-data.v1.json", + severitynumber: 2, + severitytext: "INFO", + traceparent: "00-4d678967f96e353c07a0a31c1849b500-07f83ba58dd8df70-01", + data: { + "notify-payload": { + "notify-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", + }, + }, + "notify-metadata": { + teamResponsible: "Team 1", + notifyDomain: "Delivering", + microservice: "core-event-publisher", + repositoryUrl: "https://github.com/NHSDigital/comms-mgr", + accountId: "123456789012", + environment: "development", + instance: "primary", + microserviceInstanceId: "lambda-abc123", + microserviceVersion: "1.0.0", + }, }, - }); + }, + }; - 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", async () => { + const result = await handler(validMessageStatusEvent); + + expect(result).toHaveLength(1); + expect(result[0]).toHaveProperty("transformedPayload"); + expect(result[0].transformedPayload.data[0].type).toBe("MessageStatus"); + expect(result[0].transformedPayload.data[0].attributes.messageStatus).toBe( + "delivered", + ); }); - it("extracts from an array with nested body", async () => { - const eventArray = [ - { - messageId: "123", - body: JSON.stringify({ - body: { - dataschemaversion: "1.0", - type: "uk.nhs.notify.client-callbacks.test-sid", - }, - }), - }, - ]; + it("should handle array of events", async () => { + const events = [validMessageStatusEvent]; + const result = await handler(events); - const result = await handler(eventArray); - expect(result).toEqual({ - body: { - dataschemaversion: "1.0", - type: "uk.nhs.notify.client-callbacks.test-sid", - }, - }); + expect(result).toHaveLength(1); + expect(result[0]).toHaveProperty("transformedPayload"); }); - it("returns empty body if fields are missing", async () => { - const event = { some: "random" }; - const result = await handler(event); - expect(result).toEqual({ body: {} }); + it("should handle stringified event", async () => { + const eventStr = JSON.stringify(validMessageStatusEvent); + const result = await handler(eventStr); + + expect(result).toHaveLength(1); + expect(result[0]).toHaveProperty("transformedPayload"); }); - it("handles deeply nested fields", async () => { - const event = { - level1: { - level2: { - body: JSON.stringify({ - body: { - dataschemaversion: "2.0", - type: "nested-type", - }, - }), - }, - }, + it("should throw validation error for invalid event", async () => { + const invalidEvent = { + ...validMessageStatusEvent, }; + // @ts-expect-error - Testing invalid event + delete invalidEvent.profileversion; - const result = await handler(event); - expect(result).toEqual({ - body: { - dataschemaversion: "2.0", - type: "nested-type", - }, - }); + await expect(handler(invalidEvent)).rejects.toThrow( + "profileversion is required", + ); }); - it("handles invalid JSON gracefully", async () => { - const eventStr = "{ invalid json "; - const result = await handler(eventStr); - expect(result).toEqual({ body: {} }); + it("should throw error for unsupported event type", async () => { + const unsupportedEvent = { + ...validMessageStatusEvent, + type: "uk.nhs.notify.client-callbacks.unsupported.v1", + }; + + await expect(handler(unsupportedEvent)).rejects.toThrow( + "Unsupported event type", + ); }); }); diff --git a/lambdas/client-transform-filter-lambda/src/__tests__/models/status-transition-event.test.ts b/lambdas/client-transform-filter-lambda/src/__tests__/models/status-transition-event.test.ts new file mode 100644 index 0000000..19f1244 --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/__tests__/models/status-transition-event.test.ts @@ -0,0 +1,13 @@ +import { EventTypes } from "models/status-transition-event"; + +// coverage purposes +describe("EventTypes", () => { + it("should match the expected event type values", () => { + expect(EventTypes).toEqual({ + MESSAGE_STATUS_TRANSITIONED: + "uk.nhs.notify.client-callbacks.message.status.transitioned.v1", + CHANNEL_STATUS_TRANSITIONED: + "uk.nhs.notify.client-callbacks.channel.status.transitioned.v1", + }); + }); +}); 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..7cd358f --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/__tests__/transformers/channel-status-transformer.test.ts @@ -0,0 +1,322 @@ +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 = { + profileversion: "1.0.0", + profilepublished: "2025-10", + specversion: "1.0", + id: "771f9510-f39c-52e5-b827-557766552222", + source: + "/nhs/england/notify/development/primary/data-plane/client-callbacks", + subject: + "customer/920fca11-596a-4eca-9c47-99f624614658/message/msg-789-xyz/channel/nhsapp", + type: "uk.nhs.notify.client-callbacks.channel.status.transitioned.v1", + time: "2026-02-05T14:30:00.000Z", + recordedtime: "2026-02-05T14:30:00.150Z", + datacontenttype: "application/json", + dataschema: "https://nhs.uk/schemas/notify/channel-status-data.v1.json", + severitynumber: 2, + severitytext: "INFO", + traceparent: "00-4d678967f96e353c07a0a31c1849b500-07f83ba58dd8df70-02", + data: { + "notify-payload": { + "notify-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, + }, + "notify-metadata": { + teamResponsible: "Team 1", + notifyDomain: "Delivering", + microservice: "core-event-publisher", + repositoryUrl: "https://github.com/NHSDigital/comms-mgr", + accountId: "123456789012", + environment: "development", + instance: "primary", + microserviceInstanceId: "lambda-abc123", + microserviceVersion: "1.0.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: "771f9510-f39c-52e5-b827-557766552222", + }, + }, + ], + }); + }); + + 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: { + "notify-payload": { + ...channelStatusEvent.data["notify-payload"], + "notify-data": { + ...channelStatusEvent.data["notify-payload"]["notify-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: { + "notify-payload": { + ...channelStatusEvent.data["notify-payload"], + "notify-data": { + ...channelStatusEvent.data["notify-payload"]["notify-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: { + "notify-payload": { + ...channelStatusEvent.data["notify-payload"], + "notify-data": { + ...channelStatusEvent.data["notify-payload"]["notify-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: { + "notify-payload": { + ...channelStatusEvent.data["notify-payload"], + "notify-data": { + ...channelStatusEvent.data["notify-payload"]["notify-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( + "771f9510-f39c-52e5-b827-557766552222", + ); + }); + + 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: { + "notify-payload": { + ...channelStatusEvent.data["notify-payload"], + "notify-data": { + ...channelStatusEvent.data["notify-payload"]["notify-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: { + "notify-payload": { + ...channelStatusEvent.data["notify-payload"], + "notify-data": { + ...channelStatusEvent.data["notify-payload"]["notify-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..de21fba --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/__tests__/transformers/message-status-transformer.test.ts @@ -0,0 +1,263 @@ +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 = { + profileversion: "1.0.0", + profilepublished: "2025-10", + 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", + recordedtime: "2026-02-05T14:30:00.150Z", + datacontenttype: "application/json", + dataschema: "https://nhs.uk/schemas/notify/message-status-data.v1.json", + severitynumber: 2, + severitytext: "INFO", + traceparent: "00-4d678967f96e353c07a0a31c1849b500-07f83ba58dd8df70-01", + data: { + "notify-payload": { + "notify-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", + }, + }, + "notify-metadata": { + teamResponsible: "Team 1", + notifyDomain: "Delivering", + microservice: "core-event-publisher", + repositoryUrl: "https://github.com/NHSDigital/comms-mgr", + accountId: "123456789012", + environment: "development", + instance: "primary", + microserviceInstanceId: "lambda-abc123", + microserviceVersion: "1.0.0", + }, + }, + }, + }; + + 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: { + "notify-payload": { + ...messageStatusEvent.data["notify-payload"], + "notify-data": { + ...messageStatusEvent.data["notify-payload"]["notify-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: { + "notify-payload": { + ...messageStatusEvent.data["notify-payload"], + "notify-data": { + ...messageStatusEvent.data["notify-payload"]["notify-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: { + "notify-payload": { + ...messageStatusEvent.data["notify-payload"], + "notify-data": { + ...messageStatusEvent.data["notify-payload"]["notify-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..98c8875 --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/__tests__/validators/event-validator.test.ts @@ -0,0 +1,587 @@ +/* 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"; + +describe("event-validator", () => { + describe("validateStatusTransitionEvent", () => { + const validMessageStatusEvent: StatusTransitionEvent = { + profileversion: "1.0.0", + profilepublished: "2025-10", + 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", + recordedtime: "2026-02-05T14:30:00.150Z", + datacontenttype: "application/json", + dataschema: "https://nhs.uk/schemas/notify/message-status-data.v1.json", + severitynumber: 2, + severitytext: "INFO", + traceparent: "00-4d678967f96e353c07a0a31c1849b500-07f83ba58dd8df70-01", + data: { + "notify-payload": { + "notify-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", + }, + }, + "notify-metadata": { + teamResponsible: "Team 1", + notifyDomain: "Delivering", + microservice: "core-event-publisher", + repositoryUrl: "https://github.com/NHSDigital/comms-mgr", + accountId: "123456789012", + environment: "development", + instance: "primary", + microserviceInstanceId: "lambda-abc123", + microserviceVersion: "1.0.0", + }, + }, + }, + }; + + it("should validate a valid message status event", () => { + expect(() => + validateStatusTransitionEvent(validMessageStatusEvent), + ).not.toThrow(); + }); + + describe("NHS Notify extension attributes validation", () => { + it("should throw error if profileversion is missing", () => { + const invalidEvent = { ...validMessageStatusEvent }; + // @ts-expect-error - Testing invalid event + delete invalidEvent.profileversion; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "profileversion is required", + ); + }); + + it("should throw error if profileversion is not '1.0.0'", () => { + const invalidEvent = { + ...validMessageStatusEvent, + profileversion: "2.0.0", + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "profileversion must be '1.0.0'", + ); + }); + + it("should throw error if profilepublished is missing", () => { + const invalidEvent = { ...validMessageStatusEvent }; + // @ts-expect-error - Testing invalid event + delete invalidEvent.profilepublished; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "profilepublished is required", + ); + }); + + it("should throw error if profilepublished format is invalid", () => { + const invalidEvent = { + ...validMessageStatusEvent, + profilepublished: "2025", + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "profilepublished must be in format YYYY-MM", + ); + }); + + it("should throw error if recordedtime is missing", () => { + const invalidEvent = { ...validMessageStatusEvent }; + // @ts-expect-error - Testing invalid event + delete invalidEvent.recordedtime; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "recordedtime is required", + ); + }); + + it("should throw error if recordedtime is not valid RFC 3339 format", () => { + const invalidEvent = { + ...validMessageStatusEvent, + recordedtime: "2026-02-05", + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "recordedtime must be a valid RFC 3339 timestamp", + ); + }); + + it("should throw error if recordedtime is before time", () => { + const invalidEvent = { + ...validMessageStatusEvent, + time: "2026-02-05T14:30:00.000Z", + recordedtime: "2026-02-05T14:29:00.000Z", + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "recordedtime must be >= time", + ); + }); + + it("should throw error if severitynumber is missing", () => { + const invalidEvent = { ...validMessageStatusEvent }; + // @ts-expect-error - Testing invalid event + delete invalidEvent.severitynumber; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "severitynumber is required", + ); + }); + + it("should throw error if severitytext is missing", () => { + const invalidEvent = { ...validMessageStatusEvent }; + // @ts-expect-error - Testing invalid event + delete invalidEvent.severitytext; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "severitytext is required", + ); + }); + + it("should throw error if traceparent is missing", () => { + const invalidEvent = { ...validMessageStatusEvent }; + // @ts-expect-error - Testing invalid event + delete invalidEvent.traceparent; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "traceparent is required", + ); + }); + }); + + 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( + "type must match namespace uk.nhs.notify.client-callbacks.*", + ); + }); + }); + + describe("datacontenttype validation", () => { + it("should throw error if datacontenttype is not 'application/json'", () => { + const invalidEvent = { + ...validMessageStatusEvent, + datacontenttype: "text/plain", + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "datacontenttype must be 'application/json'", + ); + }); + }); + + describe("notify-payload wrapper validation", () => { + it("should throw error if data is missing", () => { + const invalidEvent = { ...validMessageStatusEvent }; + // @ts-expect-error - Testing invalid event + delete invalidEvent.data; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "data is required", + ); + }); + + it("should throw error if notify-payload is missing", () => { + const invalidEvent = { + ...validMessageStatusEvent, + data: {} as any, + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "data.notify-payload is required", + ); + }); + + it("should throw error if notify-data is missing", () => { + const invalidEvent = { + ...validMessageStatusEvent, + data: { + "notify-payload": { + "notify-metadata": + validMessageStatusEvent.data["notify-payload"][ + "notify-metadata" + ], + } as any, + }, + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "data.notify-payload.notify-data is required", + ); + }); + + it("should throw error if notify-metadata is missing", () => { + const invalidEvent = { + ...validMessageStatusEvent, + data: { + "notify-payload": { + "notify-data": + validMessageStatusEvent.data["notify-payload"]["notify-data"], + } as any, + }, + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "data.notify-payload.notify-metadata is required", + ); + }); + }); + + describe("notify-data required fields validation", () => { + it("should throw error if notify-data.clientId is missing", () => { + const invalidEvent = { + ...validMessageStatusEvent, + data: { + "notify-payload": { + "notify-data": { + ...validMessageStatusEvent.data["notify-payload"][ + "notify-data" + ], + clientId: undefined, + }, + "notify-metadata": + validMessageStatusEvent.data["notify-payload"][ + "notify-metadata" + ], + }, + }, + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "notify-data.clientId is required", + ); + }); + + it("should throw error if notify-data.messageId is missing", () => { + const invalidEvent = { + ...validMessageStatusEvent, + data: { + "notify-payload": { + "notify-data": { + ...validMessageStatusEvent.data["notify-payload"][ + "notify-data" + ], + messageId: undefined, + }, + "notify-metadata": + validMessageStatusEvent.data["notify-payload"][ + "notify-metadata" + ], + }, + }, + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "notify-data.messageId is required", + ); + }); + + it("should throw error if notify-data.timestamp is missing", () => { + const invalidEvent = { + ...validMessageStatusEvent, + data: { + "notify-payload": { + "notify-data": { + ...validMessageStatusEvent.data["notify-payload"][ + "notify-data" + ], + timestamp: undefined, + }, + "notify-metadata": + validMessageStatusEvent.data["notify-payload"][ + "notify-metadata" + ], + }, + }, + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "notify-data.timestamp is required", + ); + }); + + it("should throw error if notify-data.timestamp is not valid RFC 3339 format", () => { + const invalidEvent = { + ...validMessageStatusEvent, + data: { + "notify-payload": { + "notify-data": { + ...validMessageStatusEvent.data["notify-payload"][ + "notify-data" + ], + timestamp: "2026-02-05", + }, + "notify-metadata": + validMessageStatusEvent.data["notify-payload"][ + "notify-metadata" + ], + }, + }, + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "notify-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: { + "notify-payload": { + "notify-data": { + ...validMessageStatusEvent.data["notify-payload"][ + "notify-data" + ], + messageStatus: undefined, + }, + "notify-metadata": + validMessageStatusEvent.data["notify-payload"][ + "notify-metadata" + ], + }, + }, + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "notify-data.messageStatus is required for message status events", + ); + }); + + it("should throw error if channels array is missing for message status event", () => { + const invalidEvent = { + ...validMessageStatusEvent, + data: { + "notify-payload": { + "notify-data": { + ...validMessageStatusEvent.data["notify-payload"][ + "notify-data" + ], + channels: undefined, + }, + "notify-metadata": + validMessageStatusEvent.data["notify-payload"][ + "notify-metadata" + ], + }, + }, + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "notify-data.channels is required for message status events", + ); + }); + + it("should throw error if channels array is empty", () => { + const invalidEvent = { + ...validMessageStatusEvent, + data: { + "notify-payload": { + "notify-data": { + ...validMessageStatusEvent.data["notify-payload"][ + "notify-data" + ], + channels: [], + }, + "notify-metadata": + validMessageStatusEvent.data["notify-payload"][ + "notify-metadata" + ], + }, + }, + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "notify-data.channels must have at least one channel", + ); + }); + + it("should throw error if channel.type is missing", () => { + const invalidEvent = { + ...validMessageStatusEvent, + data: { + "notify-payload": { + "notify-data": { + ...validMessageStatusEvent.data["notify-payload"][ + "notify-data" + ], + channels: [{ channelStatus: "delivered" } as any], + }, + "notify-metadata": + validMessageStatusEvent.data["notify-payload"][ + "notify-metadata" + ], + }, + }, + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "notify-data.channels[0].type is required", + ); + }); + + it("should throw error if channel.channelStatus is missing", () => { + const invalidEvent = { + ...validMessageStatusEvent, + data: { + "notify-payload": { + "notify-data": { + ...validMessageStatusEvent.data["notify-payload"][ + "notify-data" + ], + channels: [{ type: "nhsapp" } as any], + }, + "notify-metadata": + validMessageStatusEvent.data["notify-payload"][ + "notify-metadata" + ], + }, + }, + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "notify-data.channels[0].channelStatus is required", + ); + }); + }); + + describe("channel status specific validation", () => { + const validChannelStatusEvent: StatusTransitionEvent = { + ...validMessageStatusEvent, + type: "uk.nhs.notify.client-callbacks.channel.status.transitioned.v1", + data: { + "notify-payload": { + "notify-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, + routingPlan: { + id: "routing-plan-123", + name: "NHS App with SMS fallback", + version: "ztoe2qRAM8M8vS0bqajhyEBcvXacrGPp", + createdDate: "2023-11-17T14:27:51.413Z", + }, + }, + "notify-metadata": + validMessageStatusEvent.data["notify-payload"]["notify-metadata"], + }, + }, + }; + + 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: { + "notify-payload": { + "notify-data": { + ...validChannelStatusEvent.data["notify-payload"][ + "notify-data" + ], + channel: undefined, + }, + "notify-metadata": + validChannelStatusEvent.data["notify-payload"][ + "notify-metadata" + ], + }, + }, + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "notify-data.channel is required for channel status events", + ); + }); + + it("should throw error if channelStatus is missing for channel status event", () => { + const invalidEvent = { + ...validChannelStatusEvent, + data: { + "notify-payload": { + "notify-data": { + ...validChannelStatusEvent.data["notify-payload"][ + "notify-data" + ], + channelStatus: undefined, + }, + "notify-metadata": + validChannelStatusEvent.data["notify-payload"][ + "notify-metadata" + ], + }, + }, + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "notify-data.channelStatus is required for channel status events", + ); + }); + + it("should throw error if supplierStatus is missing for channel status event", () => { + const invalidEvent = { + ...validChannelStatusEvent, + data: { + "notify-payload": { + "notify-data": { + ...validChannelStatusEvent.data["notify-payload"][ + "notify-data" + ], + supplierStatus: undefined, + }, + "notify-metadata": + validChannelStatusEvent.data["notify-payload"][ + "notify-metadata" + ], + }, + }, + }; + + expect(() => validateStatusTransitionEvent(invalidEvent)).toThrow( + "notify-data.supplierStatus is required for channel status events", + ); + }); + }); + }); +}); diff --git a/lambdas/client-transform-filter-lambda/src/index.ts b/lambdas/client-transform-filter-lambda/src/index.ts index 91bfa94..2fab9eb 100644 --- a/lambdas/client-transform-filter-lambda/src/index.ts +++ b/lambdas/client-transform-filter-lambda/src/index.ts @@ -1,57 +1,240 @@ -export const handler = async (event: any) => { - // eslint-disable-next-line no-console - console.log("RAW EVENT:", JSON.stringify(event, null, 2)); +/** + * Transform & Filter Lambda Handler + * + * Receives events from SQS via EventBridge Pipe, validates, transforms, + * and returns filtered events for delivery to client webhooks. + * + */ - 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: {} }; - } - - let dataschemaversion: string | undefined; - let type: string | undefined; - - 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; - - 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); - } - } +import type { StatusTransitionEvent } from "models/status-transition-event"; +import { EventTypes } from "models/status-transition-event"; +import type { MessageStatusData } from "models/message-status-data"; +import type { ChannelStatusData } from "models/channel-status-data"; +import { validateStatusTransitionEvent } from "services/validators/event-validator"; +import { transformMessageStatus } from "services/transformers/message-status-transformer"; +import { transformChannelStatus } from "services/transformers/channel-status-transformer"; +import { + extractCorrelationId, + logLifecycleEvent, + logger, +} from "services/logger"; +import { + TransformationError, + ValidationError, + wrapUnknownError, +} from "services/error-handler"; +import { metricsService } from "services/metrics"; + +/** + * Parse incoming event payload into array of events + */ +function parseEventPayload(event: any): any[] { + if (Array.isArray(event)) { + return event; } + if (typeof event === "string") { + return [JSON.parse(event)]; + } + return [event]; +} - if (Array.isArray(parsedEvent)) { - for (const item of parsedEvent) findFields(item); - } else { - findFields(parsedEvent); +/** + * Transform event based on its type + */ +function transformEvent( + rawEvent: any, + eventType: string, + correlationId: string | undefined, +): any { + 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, + rawEvent.id, + ); +} + +/** + * Process a single event: validate, transform, emit metrics + */ +async function processSingleEvent(rawEvent: any): Promise { + const correlationId = extractCorrelationId(rawEvent); + logger.addContext({ correlationId }); + + logLifecycleEvent("received", { + correlationId, + eventType: rawEvent.type, + }); + + // Validate event schema + validateStatusTransitionEvent(rawEvent); - if (!dataschemaversion || !type) { - // eslint-disable-next-line no-console - console.error("Failed to extract payload from event!"); - return { body: {} }; + const eventType = rawEvent.type; + if (!eventType) { + throw new ValidationError( + "Event type is required", + correlationId, + rawEvent.id, + ); } - return { - body: { - dataschemaversion, - type, - }, + const clientId = rawEvent.data?.["notify-payload"]?.["notify-data"]?.clientId; + + // Emit metric for event received + await metricsService.emitEventReceived( + eventType ?? "unknown", + clientId ?? "unknown", + ); + + logLifecycleEvent("transformation-started", { + correlationId, + eventType, + clientId, + }); + + // Transform based on event type + const callbackPayload = transformEvent(rawEvent, eventType, correlationId); + + logLifecycleEvent("transformation-completed", { + correlationId, + eventType, + clientId, + }); + + // Emit metric for successful transformation + await metricsService.emitTransformationSuccess( + eventType, + clientId || "unknown", + ); + + // For US1, we pass all transformed events through + // US2 will add subscription filtering logic here + const transformedEvent = { + ...rawEvent, + transformedPayload: callbackPayload, }; + + logLifecycleEvent("delivery-initiated", { + correlationId, + eventType, + clientId, + }); + + // Emit metric for callback delivery initiated + await metricsService.emitDeliveryInitiated(clientId || "unknown"); + + // Clear context for next event + logger.clearContext(); + + return transformedEvent; +} + +/** + * Handle errors from event processing + */ +async function handleEventError( + error: unknown, + correlationId: string, + eventType: string, + rawEvent: any, +): Promise { + const eventCorrelationId = correlationId || "unknown"; + const eventErrorType = eventType || "unknown"; + + if (error instanceof ValidationError) { + logger.error("Event validation failed", { + correlationId: eventCorrelationId, + error: error instanceof Error ? error : new Error(String(error)), + }); + await metricsService.emitValidationError(eventErrorType); + throw error; + } + + if (error instanceof TransformationError) { + logger.error("Event transformation failed", { + correlationId: eventCorrelationId, + eventType: eventErrorType, + error: error instanceof Error ? error : new Error(String(error)), + }); + await metricsService.emitTransformationFailure( + eventErrorType, + "TransformationError", + ); + throw error; + } + + // Unknown errors + const wrappedError = wrapUnknownError( + error, + eventCorrelationId, + rawEvent?.id, + ); + logger.error("Unexpected error processing event", { + correlationId: eventCorrelationId, + error: wrappedError, + }); + await metricsService.emitTransformationFailure( + eventErrorType, + "UnknownError", + ); + throw wrappedError; +} + +/** + * Lambda handler entry point + * + * Processes events from EventBridge Pipe (SQS source). + * Returns transformed events for routing to Callbacks Event Bus. + */ +export const handler = async (event: any): Promise => { + const startTime = Date.now(); + let correlationId: string | undefined; + let eventType: string | undefined; + + try { + const parsedEvents = parseEventPayload(event); + const transformedEvents: any[] = []; + + // Process each event in the batch + for (const rawEvent of parsedEvents) { + try { + correlationId = extractCorrelationId(rawEvent); + eventType = rawEvent.type; + const transformedEvent = await processSingleEvent(rawEvent); + transformedEvents.push(transformedEvent); + } catch (error) { + await handleEventError( + error, + correlationId || "unknown", + eventType || "unknown", + rawEvent, + ); + } + } + + // Emit processing latency metric + const processingTime = Date.now() - startTime; + if (eventType) { + await metricsService.emitProcessingLatency(processingTime, eventType); + } + + // Return transformed events for EventBridge Pipe to route to Callbacks Event Bus + return transformedEvents; + } catch (error) { + // Top-level error handler + logger.error("Lambda execution failed", { + correlationId, + error: error instanceof Error ? error : new Error(String(error)), + }); + + // Rethrow to trigger Lambda retry or DLQ routing + throw error; + } }; 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..d0d11e6 --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/models/channel-status-data.ts @@ -0,0 +1,23 @@ +/** + * Channel-level status transition event data. + */ +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..8844c7f --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/models/channel-types.ts @@ -0,0 +1,4 @@ +/** + * Communication channel types + */ +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..0aaddd2 --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/models/client-callback-payload.ts @@ -0,0 +1,59 @@ +/** + * Message/Channel Status Callback payload delivered to client webhooks. + */ + +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..59c7086 --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/models/client-config.ts @@ -0,0 +1,49 @@ +/** + * Client callback subscription configuration. + * Array of subscription rules (one per event type/channel combination). + */ + +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/message-status-data.ts b/lambdas/client-transform-filter-lambda/src/models/message-status-data.ts new file mode 100644 index 0000000..84c3673 --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/models/message-status-data.ts @@ -0,0 +1,23 @@ +/** + * Message-level status transition event data. + */ +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..43efd3f --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/models/status-transition-event.ts @@ -0,0 +1,36 @@ +import type { MessageStatusData } from "models/message-status-data"; +import type { ChannelStatusData } from "models/channel-status-data"; + +/** + * Status transition event following CloudEvents 2025-11-draft standard. + * Represents a status transition notification published by Core domain to the Shared Event Bus. + * + * @template T - The type of data payload (MessageStatusData or ChannelStatusData) + */ +export interface StatusTransitionEvent< + T = MessageStatusData | ChannelStatusData, +> { + specversion: string; + id: string; + source: string; + subject: string; + type: string; + time: string; + recordedtime: 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..a4d104f --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/models/status-types.ts @@ -0,0 +1,40 @@ +/** + * Message-level statuses + */ +export type MessageStatus = + | "CREATED" + | "PENDING_ENRICHMENT" + | "ENRICHED" + | "SENDING" + | "DELIVERED" + | "FAILED"; + +/** + * Channel-level statuses + */ +export type ChannelStatus = + | "CREATED" + | "SENDING" + | "DELIVERED" + | "FAILED" + | "SKIPPED"; + +/** + * Supplier-reported statuses + */ +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/error-handler.ts b/lambdas/client-transform-filter-lambda/src/services/error-handler.ts new file mode 100644 index 0000000..f932c0f --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/services/error-handler.ts @@ -0,0 +1,191 @@ +/** + * Error handler for Lambda function with structured error responses. + * + * Distinguishes between: + * - Validation errors: Log and fail without retry + * - Config loading errors: Retriable transient failures + * - Transformation errors: Non-retriable business logic failures + * + * All errors include errorType, message, correlationId, and eventId fields. + */ + +/* eslint-disable max-classes-per-file */ + +export enum ErrorType { + VALIDATION_ERROR = "ValidationError", + CONFIG_LOADING_ERROR = "ConfigLoadingError", + TRANSFORMATION_ERROR = "TransformationError", + UNKNOWN_ERROR = "UnknownError", +} + +export interface StructuredError { + errorType: ErrorType; + message: string; + correlationId?: string; + eventId?: string; + retryable: boolean; + originalError?: Error | string; +} + +/** + * Base class for custom Lambda errors + */ +export class LambdaError extends Error { + public readonly errorType: ErrorType; + + public readonly correlationId?: string; + + public readonly eventId?: string; + + public readonly retryable: boolean; + + constructor( + errorType: ErrorType, + message: string, + correlationId?: string, + eventId?: string, + retryable = false, + ) { + super(message); + this.name = this.constructor.name; + this.errorType = errorType; + this.correlationId = correlationId; + this.eventId = eventId; + 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); + } + } + + toJSON(): StructuredError { + return { + errorType: this.errorType, + message: this.message, + correlationId: this.correlationId, + eventId: this.eventId, + retryable: this.retryable, + originalError: this.message, + }; + } +} + +/** + * Validation error - event schema is invalid + * Not retriable - event will never be valid + */ +export class ValidationError extends LambdaError { + constructor(message: string, correlationId?: string, eventId?: string) { + super(ErrorType.VALIDATION_ERROR, message, correlationId, eventId, false); + } +} + +/** + * Config loading error - S3 fetch or parse failure + * Retriable - transient AWS service failure + */ +export class ConfigLoadingError extends LambdaError { + constructor(message: string, correlationId?: string, eventId?: string) { + super( + ErrorType.CONFIG_LOADING_ERROR, + message, + correlationId, + eventId, + true, + ); + } +} + +/** + * Transformation error - unable to transform event to callback payload + * Not retriable - transformation logic issue or missing required field + */ +export class TransformationError extends LambdaError { + constructor(message: string, correlationId?: string, eventId?: string) { + super( + ErrorType.TRANSFORMATION_ERROR, + message, + correlationId, + eventId, + false, + ); + } +} + +/** + * Wraps an unknown error in structured format + */ +export function wrapUnknownError( + error: unknown, + correlationId?: string, + eventId?: string, +): LambdaError { + if (error instanceof LambdaError) { + return error; + } + + if (error instanceof Error) { + return new LambdaError( + ErrorType.UNKNOWN_ERROR, + error.message, + correlationId, + eventId, + false, + ); + } + + return new LambdaError( + ErrorType.UNKNOWN_ERROR, + String(error), + correlationId, + eventId, + false, + ); +} + +/** + * Determines if an error should trigger Lambda retry + */ +export function isRetriable(error: unknown): boolean { + if (error instanceof LambdaError) { + return error.retryable; + } + + // Unknown errors are not retriable by default + return false; +} + +/** + * Formats error for CloudWatch logging + */ +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, + }; + } + + return { + errorType: ErrorType.UNKNOWN_ERROR, + message: String(error), + retryable: false, + }; +} 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..592ad7c --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/services/logger.ts @@ -0,0 +1,138 @@ +/** + * Structured logger with correlation ID support for Lambda function. + * + * Uses Pino for high-performance JSON logging. + * Ensures dynamic data is extracted as separate log fields rather than + * embedded in description text, enabling CloudWatch Insights queries. + */ + +import pino from "pino"; + +export interface LogContext { + correlationId?: string; + clientId?: string; + eventId?: string; + eventType?: string; + messageId?: string; + statusCode?: number; + error?: Error | string; + [key: string]: any; +} + +// Create base Pino logger configured for AWS Lambda +const basePinoLogger = pino({ + level: process.env.LOG_LEVEL || "info", + formatters: { + level: (label: string) => { + return { level: label.toUpperCase() }; + }, + }, + timestamp: () => `,"timestamp":"${new Date().toISOString()}"`, + // Disable pretty printing in production and test for structured JSON logs + ...(process.env.NODE_ENV !== "production" && + process.env.NODE_ENV !== "test" && { + transport: { + target: "pino-pretty", + options: { + colorize: true, + }, + }, + }), +}); + +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; + } + } + + /** + * Add persistent context that will be included in all subsequent log entries + */ + addContext(context: LogContext): void { + this.context = { ...this.context, ...context }; + // Create a new child logger with the updated context + this.pinoLogger = basePinoLogger.child(this.context); + } + + /** + * Clear correlation ID and other transient context + */ + clearContext(): void { + this.context = {}; + this.pinoLogger = basePinoLogger; + } + + /** + * Log an informational message + */ + info(message: string, additionalContext?: LogContext): void { + this.pinoLogger.info(additionalContext || {}, message); + } + + /** + * Log a warning message + */ + warn(message: string, additionalContext?: LogContext): void { + this.pinoLogger.warn(additionalContext || {}, message); + } + + /** + * Log an error message + */ + error(message: string, additionalContext?: LogContext): void { + this.pinoLogger.error(additionalContext || {}, message); + } + + /** + * Log a debug message (only in non-production environments) + */ + debug(message: string, additionalContext?: LogContext): void { + this.pinoLogger.debug(additionalContext || {}, message); + } +} + +// Export singleton instance for convenience +export const logger = new Logger(); + +/** + * Extract correlation ID from CloudEvents event + */ +export function extractCorrelationId(event: any): string | undefined { + // CloudEvents id field serves as correlation ID + if (event?.id) { + return event.id; + } + + // Fallback to traceparent if id not present + if (event?.traceparent) { + return event.traceparent; + } + + return undefined; +} + +/** + * Log lifecycle event for end-to-end tracing + */ +export function logLifecycleEvent( + stage: + | "received" + | "transformation-started" + | "transformation-completed" + | "delivery-initiated" + | "delivery-completed" + | "dlq-placement" + | "filtered-out", + context: LogContext, +): void { + logger.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..ac17d47 --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/services/metrics.ts @@ -0,0 +1,198 @@ +/** + * CloudWatch metrics emission for Lambda function. + * + * Emits custom metrics for: + * - Event processing rates (per event type, per client) + * - Transformation success/failure counts + * - Filtering decisions (matched/rejected) + * - Error rates by error type + */ + +import { + CloudWatchClient, + PutMetricDataCommand, + StandardUnit, +} from "@aws-sdk/client-cloudwatch"; +import { logger } from "services/logger"; +import { formatErrorForLogging } from "services/error-handler"; + +export interface MetricDimensions { + EventType?: string; + ClientId?: string; + ErrorType?: string; + Environment?: string; +} + +export class MetricsService { + private cloudWatchClient: CloudWatchClient; + + private namespace: string; + + private environment: string; + + constructor() { + this.cloudWatchClient = new CloudWatchClient({ + region: process.env.AWS_REGION || "eu-west-2", + }); + this.namespace = + process.env.METRICS_NAMESPACE || "NHS-Notify/ClientCallbacks"; + this.environment = process.env.ENVIRONMENT || "development"; + } + + /** + * Emit metric for event received from Shared Event Bus + */ + async emitEventReceived(eventType: string, clientId: string): Promise { + await this.putMetric("EventsReceived", 1, { + EventType: eventType, + ClientId: clientId, + Environment: this.environment, + }); + } + + /** + * Emit metric for successful event transformation + */ + async emitTransformationSuccess( + eventType: string, + clientId: string, + ): Promise { + await this.putMetric("TransformationsSuccessful", 1, { + EventType: eventType, + ClientId: clientId, + Environment: this.environment, + }); + } + + /** + * Emit metric for failed event transformation + */ + async emitTransformationFailure( + eventType: string, + errorType: string, + ): Promise { + await this.putMetric("TransformationsFailed", 1, { + EventType: eventType, + ErrorType: errorType, + Environment: this.environment, + }); + } + + /** + * Emit metric for event matched by subscription filter + */ + async emitFilterMatched(eventType: string, clientId: string): Promise { + await this.putMetric("EventsMatched", 1, { + EventType: eventType, + ClientId: clientId, + Environment: this.environment, + }); + } + + /** + * Emit metric for event rejected by subscription filter + */ + async emitFilterRejected(eventType: string, clientId: string): Promise { + await this.putMetric("EventsRejected", 1, { + EventType: eventType, + ClientId: clientId, + Environment: this.environment, + }); + } + + /** + * Emit metric for callback delivery initiated + */ + async emitDeliveryInitiated(clientId: string): Promise { + await this.putMetric("CallbacksInitiated", 1, { + ClientId: clientId, + Environment: this.environment, + }); + } + + /** + * Emit metric for validation error + */ + async emitValidationError(eventType: string): Promise { + await this.putMetric("ValidationErrors", 1, { + EventType: eventType, + ErrorType: "ValidationError", + Environment: this.environment, + }); + } + + /** + * Emit metric for processing latency (milliseconds) + */ + async emitProcessingLatency( + latency: number, + eventType: string, + ): Promise { + await this.putMetric( + "ProcessingLatency", + latency, + { + EventType: eventType, + Environment: this.environment, + }, + StandardUnit.Milliseconds, + ); + } + + /** + * Internal method to put metric data to CloudWatch + */ + private async putMetric( + metricName: string, + value: number, + dimensions: MetricDimensions, + unit: StandardUnit = StandardUnit.Count, + ): Promise { + try { + const command = new PutMetricDataCommand({ + Namespace: this.namespace, + MetricData: [ + { + MetricName: metricName, + Value: value, + Unit: unit, + Timestamp: new Date(), + Dimensions: Object.entries(dimensions).map(([Name, Value]) => ({ + Name, + Value, + })), + }, + ], + }); + + await this.cloudWatchClient.send(command); + } catch (error) { + // Log error but don't fail Lambda execution due to metrics issue + logger.error("Failed to emit CloudWatch metric", { + errorDetails: formatErrorForLogging(error), + metricName, + dimensions, + }); + } + } + + /** + * Emit metric synchronously (fire-and-forget for non-critical metrics) + */ + emitMetricAsync( + metricName: string, + value: number, + dimensions: MetricDimensions, + ): void { + this.putMetric(metricName, value, dimensions).catch((error) => { + logger.error("Failed to emit async metric", { + errorDetails: formatErrorForLogging(error), + metricName, + dimensions, + }); + }); + } +} + +// Export singleton instance +export const metricsService = new MetricsService(); 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..1c3e181 --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/services/transformers/channel-status-transformer.ts @@ -0,0 +1,71 @@ +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"; + +/** + * Transforms a Channel Status Event from the Shared Event Bus format + * to the client-facing JSON:API callback payload format. + * + * Extracts fields from notify-data section and constructs a JSON:API + * compliant payload, excluding operational fields (clientId, previousChannelStatus, previousSupplierStatus). + * + * @param event - Status transition event with ChannelStatusData + * @returns Client callback payload in JSON:API format + */ +export function transformChannelStatus( + event: StatusTransitionEvent, +): ClientCallbackPayload { + const notifyData = event.data["notify-payload"]["notify-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; + + // Build attributes object with required fields + const attributes: ChannelStatusAttributes = { + messageId: notifyData.messageId, + messageReference: notifyData.messageReference, + channel, + channelStatus, + supplierStatus, + cascadeType: notifyData.cascadeType, + cascadeOrder: notifyData.cascadeOrder, + timestamp: notifyData.timestamp, + retryCount: notifyData.retryCount, + }; + + // Include optional fields if present + if (notifyData.channelStatusDescription) { + attributes.channelStatusDescription = notifyData.channelStatusDescription; + } + + if (notifyData.channelFailureReasonCode) { + attributes.channelFailureReasonCode = notifyData.channelFailureReasonCode; + } + + // Construct JSON:API payload + 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/message-status-transformer.ts b/lambdas/client-transform-filter-lambda/src/services/transformers/message-status-transformer.ts new file mode 100644 index 0000000..e374adc --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/services/transformers/message-status-transformer.ts @@ -0,0 +1,70 @@ +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"; + +/** + * Transforms a Message Status Event from the Shared Event Bus format + * to the client-facing JSON:API callback payload format. + * + * Extracts fields from notify-data section and constructs a JSON:API + * compliant payload, excluding operational fields (clientId, previousMessageStatus). + * + * @param event - Status transition event with MessageStatusData + * @returns Client callback payload in JSON:API format + */ +export function transformMessageStatus( + event: StatusTransitionEvent, +): ClientCallbackPayload { + const notifyData = event.data["notify-payload"]["notify-data"]; + const { messageId } = notifyData; + const messageStatus = + notifyData.messageStatus.toLowerCase() as ClientMessageStatus; + const channels = notifyData.channels.map((channel) => ({ + ...channel, + type: channel.type.toLowerCase() as ClientChannel, + channelStatus: channel.channelStatus.toLowerCase() as ClientChannelStatus, + })); + + // Build attributes object with required fields + const attributes: MessageStatusAttributes = { + messageId: notifyData.messageId, + messageReference: notifyData.messageReference, + messageStatus, + channels, + timestamp: notifyData.timestamp, + routingPlan: notifyData.routingPlan, + }; + + // Include optional fields if present + if (notifyData.messageStatusDescription) { + attributes.messageStatusDescription = notifyData.messageStatusDescription; + } + + if (notifyData.messageFailureReasonCode) { + attributes.messageFailureReasonCode = notifyData.messageFailureReasonCode; + } + + // Construct JSON:API payload + 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..7dff717 --- /dev/null +++ b/lambdas/client-transform-filter-lambda/src/services/validators/event-validator.ts @@ -0,0 +1,277 @@ +import { CloudEvent, ValidationError } from "cloudevents"; +import { EventTypes } from "models/status-transition-event"; + +/** + * Validates if a string is a valid RFC 3339 timestamp + * Used for custom NHS Notify extension attributes not validated by CloudEvents SDK + */ +function isValidRFC3339(timestamp: string): boolean { + // Check basic format first with a safe pattern + if (typeof timestamp !== "string" || timestamp.length < 20) { + return false; + } + + // Verify it's a valid date using native parser + const date = new Date(timestamp); + if (Number.isNaN(date.getTime())) { + return false; + } + + // Basic format validation without potentially catastrophic regex + const parts = timestamp.split("T"); + if (parts.length !== 2) { + return false; + } + + const datePart = parts[0]; + const timePart = parts[1]; + + // Validate date part: YYYY-MM-DD + if (!/^\d{4}-\d{2}-\d{2}$/.test(datePart)) { + return false; + } + + // Validate time part has required components + const hasTimeZone = + timePart.endsWith("Z") || /[+-]\d{2}:\d{2}$/.test(timePart); + const hasTimeFormat = /^\d{2}:\d{2}:\d{2}/.test(timePart); + + return hasTimeZone && hasTimeFormat; +} + +/** + * Checks if event type is a message status event + */ +function isMessageStatusEvent(type: string): boolean { + return type === EventTypes.MESSAGE_STATUS_TRANSITIONED; +} + +/** + * Checks if event type is a channel status event + */ +function isChannelStatusEvent(type: string): boolean { + return type === EventTypes.CHANNEL_STATUS_TRANSITIONED; +} + +/** + * Validates NHS Notify-specific CloudEvents extension attributes + */ +function validateNHSNotifyExtensions(event: any): void { + // profileversion + if (!event.profileversion) { + throw new Error("profileversion is required"); + } + if (event.profileversion !== "1.0.0") { + throw new Error("profileversion must be '1.0.0'"); + } + + // profilepublished + if (!event.profilepublished) { + throw new Error("profilepublished is required"); + } + if (!/^\d{4}-\d{2}$/.test(event.profilepublished)) { + throw new Error("profilepublished must be in format YYYY-MM"); + } + + // recordedtime (optional in CloudEvents, required in NHS Notify) + if (!event.recordedtime) { + throw new Error("recordedtime is required"); + } + if (!isValidRFC3339(event.recordedtime)) { + throw new Error("recordedtime must be a valid RFC 3339 timestamp"); + } + if (new Date(event.recordedtime) < new Date(event.time)) { + throw new Error("recordedtime must be >= time"); + } + + // severitynumber + if (event.severitynumber === undefined || event.severitynumber === null) { + throw new Error("severitynumber is required"); + } + + // severitytext + if (!event.severitytext) { + throw new Error("severitytext is required"); + } + + // traceparent + if (!event.traceparent) { + throw new Error("traceparent is required"); + } +} + +/** + * Validates event type matches NHS Notify namespace + */ +function validateEventTypeNamespace(type: string): void { + if (!type.startsWith("uk.nhs.notify.client-callbacks.")) { + throw new Error( + "type must match namespace uk.nhs.notify.client-callbacks.*", + ); + } +} + +/** + * Validates notify-payload wrapper structure + */ +function validateNotifyPayloadWrapper(data: any): void { + if (!data) { + throw new Error("data is required"); + } + + if (!data["notify-payload"]) { + throw new Error("data.notify-payload is required"); + } + + if (!data["notify-payload"]["notify-data"]) { + throw new Error("data.notify-payload.notify-data is required"); + } + + if (!data["notify-payload"]["notify-metadata"]) { + throw new Error("data.notify-payload.notify-metadata is required"); + } +} + +/** + * Validates required fields in notify-data for filtering + */ +function validateNotifyDataRequiredFields(data: any): void { + const notifyData = data["notify-payload"]["notify-data"]; + + if (!notifyData.clientId) { + throw new Error("notify-data.clientId is required"); + } + + if (!notifyData.messageId) { + throw new Error("notify-data.messageId is required"); + } + + if (!notifyData.timestamp) { + throw new Error("notify-data.timestamp is required"); + } + + if (!isValidRFC3339(notifyData.timestamp)) { + throw new Error("notify-data.timestamp must be a valid RFC 3339 timestamp"); + } +} + +/** + * Validates message status specific fields + */ +function validateMessageStatusFields(data: any): void { + const notifyData = data["notify-payload"]["notify-data"]; + + if (!notifyData.messageStatus) { + throw new Error( + "notify-data.messageStatus is required for message status events", + ); + } + + if (!notifyData.channels) { + throw new Error( + "notify-data.channels is required for message status events", + ); + } + + if (!Array.isArray(notifyData.channels)) { + throw new TypeError("notify-data.channels must be an array"); + } + + if (notifyData.channels.length === 0) { + throw new Error("notify-data.channels must have at least one channel"); + } + + // Validate each channel in the array + for (let index = 0; index < notifyData.channels.length; index++) { + // eslint-disable-next-line security/detect-object-injection + const channel = notifyData.channels[index]; + if (!channel?.type) { + throw new Error(`notify-data.channels[${index}].type is required`); + } + if (!channel.channelStatus) { + throw new Error( + `notify-data.channels[${index}].channelStatus is required`, + ); + } + } +} + +/** + * Validates channel status specific fields + */ +function validateChannelStatusFields(data: any): void { + const notifyData = data["notify-payload"]["notify-data"]; + + if (!notifyData.channel) { + throw new Error( + "notify-data.channel is required for channel status events", + ); + } + + if (!notifyData.channelStatus) { + throw new Error( + "notify-data.channelStatus is required for channel status events", + ); + } + + if (!notifyData.supplierStatus) { + throw new Error( + "notify-data.supplierStatus is required for channel status events", + ); + } +} + +/** + * Validates a Status Transition Event against the CloudEvents schema + * and NHS Notify notify-payload structure. + * + * Uses the official CloudEvents SDK for standard attribute validation, + * with additional NHS Notify-specific extension and payload validation. + * + * @param event - The event to validate + * @throws Error if validation fails with detailed error message + */ +export function validateStatusTransitionEvent(event: any): void { + try { + // CloudEvent constructor validates standard CloudEvents attributes: + // - specversion (must be "1.0") + // - id (required, must be valid format) + // - source (required, must be valid URI-reference) + // - type (required, must be valid format) + // - time (if present, must be valid RFC 3339 timestamp) + // - datacontenttype (if present, must be valid media type) + const ce = new CloudEvent(event, /* strict validation */ true); + + // Validate NHS Notify-specific extension attributes + validateNHSNotifyExtensions(event); + + // Validate event type namespace + validateEventTypeNamespace(ce.type); + + // Validate datacontenttype is application/json + if (ce.datacontenttype !== "application/json") { + throw new Error("datacontenttype must be 'application/json'"); + } + + // Validate notify-payload wrapper structure + validateNotifyPayloadWrapper(ce.data); + + // Validate notify-data required fields + validateNotifyDataRequiredFields(ce.data); + + // Validate event type-specific fields + if (isMessageStatusEvent(ce.type)) { + validateMessageStatusFields(ce.data); + } else if (isChannelStatusEvent(ce.type)) { + validateChannelStatusFields(ce.data); + } + } catch (error) { + if (error instanceof ValidationError) { + throw new TypeError(`CloudEvents validation failed: ${error.message}`); + } + if (error instanceof Error) { + throw error; + } + throw new TypeError(`CloudEvents validation failed: ${String(error)}`); + } +} 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/README.md b/lambdas/mock-webhook-lambda/README.md new file mode 100644 index 0000000..633a0cb --- /dev/null +++ b/lambdas/mock-webhook-lambda/README.md @@ -0,0 +1,70 @@ +# Mock Webhook Lambda + +**Purpose**: Test infrastructure lambda that simulates a client webhook endpoint for integration testing. + +## Overview + +This Lambda acts as a mock webhook receiver for testing the callback delivery pipeline. It: + +1. Receives POST requests containing JSON:API formatted callbacks (MessageStatus or ChannelStatus) +2. Logs each received callback to CloudWatch in a structured, queryable format +3. Returns HTTP 200 OK to acknowledge receipt + +## Usage in Tests + +Integration tests can: + +1. Configure this Lambda's URL as the webhook endpoint in client subscription configuration +2. Trigger callback events through the delivery pipeline +3. Query CloudWatch Logs to verify callbacks were received with correct payloads + +## Log Format + +Each callback is logged with the pattern: + +`CALLBACK {messageId} {messageType} : {JSON payload}` + +Example: + +`CALLBACK msg-123-456 MessageStatus : {"type":"MessageStatus","id":"msg-123-456","attributes":{...}}` + +This format enables tests to: + +- Filter logs by message ID +- Parse payloads for validation +- Verify callback counts and content + +## Deployment + +This Lambda is deployed only in test/development environments as part of the integration test infrastructure. + +Quick deployment: + +```bash +# 1. Build the lambda +npm install +npm run lambda-build + +# 2. Enable in environment tfvars +# Set deploy_mock_webhook = true in your environment's .tfvars file + +# 3. Apply Terraform +cd infrastructure/terraform/components/callbacks +terraform apply -var-file=etc/dev.tfvars +``` + +**Configuration**: + +- **Runtime**: Node.js 22 +- **Handler**: `index.handler` +- **Memory**: 256 MB +- **Timeout**: 10 seconds +- **Trigger**: Function URL or API Gateway +- **Environment**: dev/test only (controlled via `deploy_mock_webhook` variable) + +## Scripts + +- `npm run lambda-build` - Bundle Lambda for deployment +- `npm test` - Run unit tests +- `npm run typecheck` - Type check without emit +- `npm run lint` - Lint code diff --git a/lambdas/mock-webhook-lambda/jest.config.ts b/lambdas/mock-webhook-lambda/jest.config.ts new file mode 100644 index 0000000..4cec36d --- /dev/null +++ b/lambdas/mock-webhook-lambda/jest.config.ts @@ -0,0 +1,63 @@ +import type { Config } from "jest"; + +export const baseJestConfig: Config = { + preset: "ts-jest", + + // Automatically clear mock calls, instances, contexts and results before every test + clearMocks: true, + + // Indicates whether the coverage information should be collected while executing the test + collectCoverage: true, + + // The directory where Jest should output its coverage files + coverageDirectory: "./.reports/unit/coverage", + + // Indicates which provider should be used to instrument code for coverage + coverageProvider: "babel", + + coverageThreshold: { + global: { + branches: 100, + functions: 100, + lines: 100, + statements: -10, + }, + }, + + coveragePathIgnorePatterns: ["/__tests__/"], + transform: { "^.+\\.ts$": "ts-jest" }, + testPathIgnorePatterns: [".build"], + testMatch: ["**/?(*.)+(spec|test).[jt]s?(x)"], + + // Use this configuration option to add custom reporters to Jest + reporters: [ + "default", + [ + "jest-html-reporter", + { + pageTitle: "Test Report", + outputPath: "./.reports/unit/test-report.html", + includeFailureMsg: true, + }, + ], + ], + + // The test environment that will be used for testing + testEnvironment: "jsdom", +}; + +const utilsJestConfig = { + ...baseJestConfig, + + testEnvironment: "node", + + coveragePathIgnorePatterns: [ + ...(baseJestConfig.coveragePathIgnorePatterns ?? []), + "zod-validators.ts", + ], + + // Mirror tsconfig's baseUrl: "src" - automatically resolves non-relative imports + modulePaths: ["/src"], +}; + +export default utilsJestConfig; 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..63f020b --- /dev/null +++ b/lambdas/mock-webhook-lambda/src/__tests__/index.test.ts @@ -0,0 +1,223 @@ +import type { APIGatewayProxyEvent } from "aws-lambda"; +import { handler } from "index"; +import type { CallbackMessage, CallbackPayload } from "types"; + +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 500 when body is invalid JSON", async () => { + const event = createMockEvent("invalid json {"); + const result = await handler(event); + + expect(result.statusCode).toBe(500); + const body = JSON.parse(result.body); + expect(body.message).toBe("Internal server error"); + }); + + 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"); + }); + }); + + 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)); + + // Capture console output (pino writes to stdout) + const logSpy = jest.spyOn(process.stdout, "write").mockImplementation(); + + await handler(event); + + expect(logSpy).toHaveBeenCalled(); + + // Find the log entry containing our callback + const logCalls = logSpy.mock.calls.map( + (call) => call[0]?.toString() || "", + ); + const callbackLog = logCalls.find((log) => + log.includes("CALLBACK test-msg-789"), + ); + + expect(callbackLog).toBeDefined(); + expect(callbackLog).toContain("MessageStatus"); + + logSpy.mockRestore(); + }); + }); +}); diff --git a/lambdas/mock-webhook-lambda/src/index.ts b/lambdas/mock-webhook-lambda/src/index.ts new file mode 100644 index 0000000..5c1b249 --- /dev/null +++ b/lambdas/mock-webhook-lambda/src/index.ts @@ -0,0 +1,101 @@ +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", +}); + +/** + * Mock webhook endpoint for testing callback delivery. + * + * Receives callbacks from API Destination and logs them to CloudWatch + * in a structured format that can be queried by integration tests. + * + * @param event - API Gateway proxy event containing callback payload + * @returns 200 OK with confirmation message, or 400 for invalid requests + */ +export async function handler( + event: APIGatewayProxyEvent, +): Promise { + const correlationId = event.requestContext?.requestId || "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; + + 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" }), + }; + } + + // 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) { + 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..4a54cf4 --- /dev/null +++ b/lambdas/mock-webhook-lambda/src/types.ts @@ -0,0 +1,23 @@ +/** + * 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"; + id: string; + attributes: Record; +} + +/** + * 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..638985b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,9 +6,16 @@ "": { "name": "nhs-notify-client-callbacks", "workspaces": [ - "lambdas/client-transform-filter-lambda" + "lambdas/client-transform-filter-lambda", + "lambdas/mock-webhook-lambda" ], + "dependencies": { + "@aws-sdk/client-cloudwatch": "^3.990.0" + }, "devDependencies": { + "@aws-sdk/client-cloudwatch-logs": "^3.991.0", + "@aws-sdk/client-eventbridge": "^3.990.0", + "@aws-sdk/client-sqs": "^3.990.0", "@stylistic/eslint-plugin": "^3.1.0", "@tsconfig/node22": "^22.0.2", "@types/jest": "^29.5.14", @@ -45,7 +52,10 @@ "name": "nhs-notify-client-transform-filter-lambda", "version": "0.0.1", "dependencies": { - "esbuild": "^0.25.0" + "@aws-sdk/client-cloudwatch": "^3.709.0", + "cloudevents": "^8.0.2", + "esbuild": "^0.25.0", + "pino": "^9.5.0" }, "devDependencies": { "@tsconfig/node22": "^22.0.2", @@ -72,6 +82,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,717 +129,1637 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", + "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==", "dev": true, - "license": "MIT", + "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" - }, - "engines": { - "node": ">=6.9.0" + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" } }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "dev": true, - "license": "MIT", + "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": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" + "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-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": ">=6.9.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/helper-string-parser": { - "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-validator-identifier": { - "version": "7.27.1", + "node_modules/@aws-sdk/client-cloudwatch": { + "version": "3.990.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch/-/client-cloudwatch-3.990.0.tgz", + "integrity": "sha512-+JV1QBPAOS66Czebzh3J9RqjuUKwY22JvrU+OUdk+005pick5ytqdXCndLMCSf0igrFYtxMfKedUPUcSgVn/OQ==", + "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.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-compression": "^4.3.29", + "@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", + "@smithy/util-waiter": "^4.2.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "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==", "dev": true, - "license": "MIT", + "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==", + "dev": true, + "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-option": { - "version": "7.27.1", + "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==", "dev": true, - "license": "MIT", + "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==", + "dev": true, + "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/helpers": { - "version": "7.27.6", - "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": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.27.6" + "@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/parser": { - "version": "7.27.5", - "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/types": "^7.27.3" - }, - "bin": { - "parser": "bin/babel-parser.js" + "@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.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-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/helper-plugin-utils": "^7.8.0" + "@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" }, - "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-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-class-properties": { - "version": "7.12.13", - "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.12.13" + "@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-static-block": { - "version": "7.14.5", - "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.14.5" + "@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" }, "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/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.27.1" + "@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-meta": { - "version": "7.10.4", - "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.10.4" + "@aws-sdk/types": "^3.973.1", + "@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-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": { - "@babel/helper-plugin-utils": "^7.8.0" + "@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-jsx": { - "version": "7.27.1", - "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.27.1" + "@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" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=20.0.0" } }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", + "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==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "@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" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", + "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==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@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-numeric-separator": { - "version": "7.10.4", - "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.10.4" + "@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" }, - "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/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": { - "@babel/helper-plugin-utils": "^7.8.0" + "@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-optional-catch-binding": { - "version": "7.8.3", + "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==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@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-optional-chaining": { - "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-private-property-in-object": { - "version": "7.14.5", + "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": { + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "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==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=20.0.0" + } + }, + "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": { + "@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-top-level-await": { - "version": "7.14.5", - "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.14.5" + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=20.0.0" + } + }, + "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": { + "@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": ">=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-typescript": { + "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": { + "@smithy/types": "^4.12.0", + "fast-xml-parser": "5.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "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" + "engines": { + "node": ">=6.9.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" - ], + "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-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-validator-identifier": { + "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-option": { + "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/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-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/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/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/@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-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-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/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-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-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" - ], + "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-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-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-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/@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-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-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/@esbuild/linux-mips64el": { + "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/@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/linux-mips64el/-/linux-mips64el-0.25.0.tgz", - "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", + "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_modules/@esbuild/linux-ppc64": { + "node_modules/@esbuild/android-arm": { "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==", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", + "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", "cpu": [ - "ppc64" + "arm" ], "optional": true, "os": [ - "linux" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/linux-riscv64": { + "node_modules/@esbuild/android-arm64": { "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": [ + "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, @@ -817,953 +1782,1664 @@ "linux" ], "engines": { - "node": ">=18" + "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/@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": ">=8" + } + }, + "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": ">=8" + } + }, + "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": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/@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/@jest/expect-utils": { + "version": "29.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, "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/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-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/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/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/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-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/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/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/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/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-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-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/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-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/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": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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": "^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/set-array": { + "version": "1.2.1", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, + "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=6.0.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==", + "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", "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": "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/@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": { + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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": { + "@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/@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, + "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": { - "@eslint/core": "^0.15.1", - "levn": "^0.4.1" + "@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 || ^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/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": { - "@types/json-schema": "^7.0.15" + "@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 || ^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==", + "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==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.12.0", + "@smithy/util-hex-encoding": "^4.2.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==", + "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==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" + "@smithy/eventstream-serde-universal": "^4.2.8", + "@smithy/types": "^4.12.0", + "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==", + "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==", "dev": true, "license": "Apache-2.0", - "engines": { - "node": ">=18.18" + "dependencies": { + "@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/module-importer": { - "version": "1.0.1", + "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==", "dev": true, "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==", + "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==", "dev": true, "license": "Apache-2.0", - "engines": { - "node": ">=18.18" + "dependencies": { + "@smithy/eventstream-codec": "^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/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "dev": true, - "license": "ISC", + "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": { - "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/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/argparse": { - "version": "1.0.10", - "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": { - "sprintf-js": "~1.0.2" + "@smithy/types": "^4.12.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.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/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": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.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/js-yaml": { - "version": "3.14.1", - "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": { - "argparse": "^1.0.7", - "esprima": "^4.0.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", + "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==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "p-locate": "^4.1.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/p-limit": { - "version": "2.3.0", - "dev": true, - "license": "MIT", + "node_modules/@smithy/middleware-compression": { + "version": "4.3.29", + "resolved": "https://registry.npmjs.org/@smithy/middleware-compression/-/middleware-compression-4.3.29.tgz", + "integrity": "sha512-ZWDXc7Sb2ONrBhc8e845e3jxreczW0CsMan8+lzryqXw9ZVDxssqlHT3pu+idoBZ79SffyoQBOp6wcw62ZQImA==", + "license": "Apache-2.0", "dependencies": { - "p-try": "^2.0.0" + "@smithy/core": "^3.23.0", + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", + "fflate": "0.8.1", + "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/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": { - "p-limit": "^2.2.0" + "@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/load-nyc-config/node_modules/resolve-from": { - "version": "5.0.0", - "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/@istanbuljs/schema": { - "version": "0.1.3", - "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": { + "@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": ">=8" + "node": ">=18.0.0" } }, - "node_modules/@jest/console": { - "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/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/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" + "node": ">=18.0.0" } }, - "node_modules/@jest/core": { - "version": "29.7.0", - "dev": true, - "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": { - "@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/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/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": { + "@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": ">=8" + "node": ">=18.0.0" } }, - "node_modules/@jest/environment": { - "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": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^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": { - "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": { - "expect": "^29.7.0", - "jest-snapshot": "^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/expect-utils": { - "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-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/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/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", + "@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/globals": { - "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": { - "@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", + "tslib": "^2.6.2" + }, + "engines": { + "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": "^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/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": { - "@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/@jest/schemas": { - "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": { - "@sinclair/typebox": "^0.27.8" + "@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/source-map": { - "version": "29.6.3", - "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": { - "@jridgewell/trace-mapping": "^0.3.18", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" + "@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-result": { - "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/console": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" + "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/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": { - "@jest/test-result": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "slash": "^3.0.0" + "@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/transform": { - "version": "29.7.0", - "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": { - "@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/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/@jest/types": { - "version": "29.6.3", - "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": { - "@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" + "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-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": { - "@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-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/set-array": { - "version": "1.2.1", - "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": { + "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-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": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "@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/@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-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": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" + "@smithy/config-resolver": "^4.4.6", + "@smithy/credential-provider-imds": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/smithy-client": "^4.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-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": { - "fast-glob": "3.3.1" + "@smithy/node-config-provider": "^4.3.8", + "@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-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": { - "@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" + "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-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": { - "is-glob": "^4.0.1" + "@smithy/types": "^4.12.0", + "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-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": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "@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.stat": { - "version": "2.0.5", - "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": { + "@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/@nodelib/fs.walk": { - "version": "1.2.8", - "dev": true, - "license": "MIT", + "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": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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": { + "@smithy/util-buffer-from": "^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-waiter": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.8.tgz", + "integrity": "sha512-n+lahlMWk+aejGuax7DPWtqav8HYnWxQwR+LCG2BgCUmaGcTe9qZCFsmw8TMg9iG75HOwhrJCX9TCJRLH+Yzqg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" }, - "funding": { - "url": "https://opencollective.com/pkgr" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@rtsao/scc": { + "node_modules/@smithy/uuid": { "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", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "dev": true, - "license": "BSD-3-Clause", + "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": { @@ -2597,6 +4273,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, @@ -2855,9 +4570,17 @@ "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" @@ -3006,6 +4729,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 +4841,6 @@ }, "node_modules/call-bind": { "version": "1.0.8", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.0", @@ -3120,7 +4857,6 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -3132,7 +4868,6 @@ }, "node_modules/call-bound": { "version": "1.0.4", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -3254,6 +4989,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 +5294,6 @@ }, "node_modules/define-data-property": { "version": "1.1.4", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -3670,7 +5443,6 @@ }, "node_modules/dunder-proto": { "version": "1.0.1", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -3806,7 +5578,6 @@ }, "node_modules/es-define-property": { "version": "1.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3814,7 +5585,6 @@ }, "node_modules/es-errors": { "version": "1.3.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3850,7 +5620,6 @@ }, "node_modules/es-object-atoms": { "version": "1.1.1", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -4919,7 +6688,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 +6731,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.4", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.4.tgz", + "integrity": "sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.19.1", "dev": true, @@ -4979,6 +6781,12 @@ "bser": "2.1.1" } }, + "node_modules/fflate": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.1.tgz", + "integrity": "sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -5077,7 +6885,6 @@ }, "node_modules/for-each": { "version": "0.3.5", - "dev": true, "license": "MIT", "dependencies": { "is-callable": "^1.2.7" @@ -5128,7 +6935,6 @@ }, "node_modules/function-bind": { "version": "1.1.2", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5184,7 +6990,6 @@ }, "node_modules/get-intrinsic": { "version": "1.3.0", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -5215,7 +7020,6 @@ }, "node_modules/get-proto": { "version": "1.0.1", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -5343,7 +7147,6 @@ }, "node_modules/gopd": { "version": "1.2.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5378,7 +7181,6 @@ }, "node_modules/has-property-descriptors": { "version": "1.0.2", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" @@ -5403,7 +7205,6 @@ }, "node_modules/has-symbols": { "version": "1.1.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5414,7 +7215,6 @@ }, "node_modules/has-tostringtag": { "version": "1.0.2", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -5428,7 +7228,6 @@ }, "node_modules/hasown": { "version": "2.0.2", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -5590,7 +7389,6 @@ }, "node_modules/inherits": { "version": "2.0.4", - "dev": true, "license": "ISC" }, "node_modules/internal-slot": { @@ -5606,6 +7404,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 +7527,6 @@ }, "node_modules/is-callable": { "version": "1.2.7", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5807,7 +7620,6 @@ }, "node_modules/is-generator-function": { "version": "1.1.0", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -5885,7 +7697,6 @@ }, "node_modules/is-regex": { "version": "1.2.1", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -5969,7 +7780,6 @@ }, "node_modules/is-typed-array": { "version": "1.1.15", - "dev": true, "license": "MIT", "dependencies": { "which-typed-array": "^1.1.16" @@ -6839,6 +8649,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 +8910,6 @@ }, "node_modules/math-intrinsics": { "version": "1.1.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7229,6 +9047,10 @@ "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 +9198,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 +9383,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 +9497,6 @@ }, "node_modules/possible-typed-array-names": { "version": "1.1.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7693,6 +9560,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 +9674,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 +9799,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 +9951,6 @@ }, "node_modules/safe-regex-test": { "version": "1.1.0", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -8049,6 +9964,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 +10015,6 @@ }, "node_modules/set-function-length": { "version": "1.2.2", - "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -8237,6 +10160,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 +10186,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 +10427,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 +10513,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 +10785,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 +11479,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 +11696,6 @@ }, "node_modules/which-typed-array": { "version": "1.1.19", - "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", diff --git a/package.json b/package.json index 2013466..de9e33d 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,8 @@ { "devDependencies": { + "@aws-sdk/client-cloudwatch-logs": "^3.991.0", + "@aws-sdk/client-eventbridge": "^3.990.0", + "@aws-sdk/client-sqs": "^3.990.0", "@stylistic/eslint-plugin": "^3.1.0", "@tsconfig/node22": "^22.0.2", "@types/jest": "^29.5.14", @@ -47,6 +50,10 @@ "typecheck": "npm run typecheck --workspaces" }, "workspaces": [ - "lambdas/client-transform-filter-lambda" - ] + "lambdas/client-transform-filter-lambda", + "lambdas/mock-webhook-lambda" + ], + "dependencies": { + "@aws-sdk/client-cloudwatch": "^3.990.0" + } } diff --git a/scripts/config/sonar-scanner.properties b/scripts/config/sonar-scanner.properties index 9fbc7fc..ae7c364 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__/** 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/tests/integration/WEBHOOK_VERIFICATION.md b/tests/integration/WEBHOOK_VERIFICATION.md new file mode 100644 index 0000000..b70627a --- /dev/null +++ b/tests/integration/WEBHOOK_VERIFICATION.md @@ -0,0 +1,199 @@ +# Integration Test Webhook Verification + +This guide explains how to verify webhook delivery in integration tests using the mock-webhook-lambda. + +## Overview + +The mock-webhook-lambda ([lambdas/mock-webhook-lambda](../../lambdas/mock-webhook-lambda/)) provides a test endpoint that: + +1. Receives callback POST requests from API Destination +2. Logs each callback to CloudWatch in a structured, queryable format +3. Returns HTTP 200 OK + +## Setup + +### 1. Deploy Mock Webhook Lambda + +Quick steps: + +```bash +# 1. Build the lambda +npm install +npm run lambda-build + +# 2. Enable in your environment tfvars +# Edit infrastructure/terraform/components/callbacks/etc/dev.tfvars +deploy_mock_webhook = true + +# 3. Deploy via Terraform +cd infrastructure/terraform/components/callbacks +terraform apply -var-file=etc/dev.tfvars + +# 4. Get outputs for test configuration +terraform output mock_webhook_url +terraform output mock_webhook_lambda_log_group_name +``` + +### 2. Configure Environment Variables + +Set these environment variables for integration tests: + +```bash +export TEST_WEBHOOK_URL=$(terraform output -raw mock_webhook_url) +export TEST_WEBHOOK_LOG_GROUP=$(terraform output -raw mock_webhook_lambda_log_group_name) +export TEST_EVENT_BUS_NAME="nhs-notify-shared-event-bus-dev" +export TEST_QUEUE_URL="https://sqs.eu-west-2.amazonaws.com/123456789012/callback-queue" +``` + +### 3. Configure Client Subscription + +Update your test client subscription configuration to point to the mock webhook: + +```json +{ + "clientId": "test-client-123", + "webhookUrl": "${TEST_WEBHOOK_URL}", + "callbackAuth": { + "type": "bearer", + "secretArn": "arn:aws:secretsmanager:..." + }, + "messageStatus": { + "enabled": true, + "statusChanges": ["delivered", "failed"] + } +} +``` + +## Usage in Tests + +### Query Callbacks from CloudWatch + +Use the helper functions in [helpers/cloudwatch-helpers.ts](./helpers/cloudwatch-helpers.ts): + +```typescript +import { + getMessageStatusCallbacks, + getChannelStatusCallbacks, + getAllCallbacks, +} from "./helpers"; + +describe("Webhook delivery verification", () => { + it("should deliver MessageStatus callback", async () => { + const messageId = "test-msg-123"; + const logGroup = process.env.TEST_WEBHOOK_LOG_GROUP!; + + // Trigger callback event... + await publishEventToEventBus(messageStatusEvent); + + // Wait for processing + await new Promise((resolve) => setTimeout(resolve, 5000)); + + // Query CloudWatch logs + const callbacks = await getMessageStatusCallbacks(logGroup, messageId); + + // Verify callback was received + expect(callbacks).toHaveLength(1); + expect(callbacks[0]).toMatchObject({ + type: "MessageStatus", + id: messageId, + attributes: expect.objectContaining({ + messageId, + messageStatus: "delivered", + messageReference: expect.any(String), + }), + }); + }); +}); +``` + +### Common Patterns + +**Check callback count:** + +```typescript +const callbacks = await getMessageStatusCallbacks(logGroup, messageId); +expect(callbacks.length).toBeGreaterThan(0); +``` + +**Verify specific status transition:** + +```typescript +const callbacks = await getMessageStatusCallbacks(logGroup, messageId); +const deliveredCallback = callbacks.find( + (cb) => cb.attributes.messageStatus === "delivered", +); +expect(deliveredCallback).toBeDefined(); +``` + +**Verify channel-specific callback:** + +```typescript +const callbacks = await getChannelStatusCallbacks(logGroup, messageId); +const nhsAppCallback = callbacks.find( + (cb) => cb.attributes.channel === "nhsapp", +); +expect(nhsAppCallback?.attributes.channelStatus).toBe("delivered"); +``` + +**Check no callbacks received (filtering):** + +```typescript +const callbacks = await getAllCallbacks(logGroup, messageId); +expect(callbacks).toHaveLength(0); +``` + +## Log Format + +The mock webhook logs callbacks in this format: + +`CALLBACK {messageId} {messageType} : {JSON payload}` + +Example CloudWatch log entry: + +```json +{ + "level": "info", + "correlationId": "abc-123", + "messageId": "msg-456", + "messageType": "MessageStatus", + "msg": "CALLBACK msg-456 MessageStatus : {\"type\":\"MessageStatus\",\"id\":\"msg-456\",\"attributes\":{...}}" +} +``` + +The helpers extract the JSON payload from the `msg` field for assertion. + +## Troubleshooting + +### No callbacks found in logs + +1. Check TEST_WEBHOOK_LOG_GROUP is correct +2. Verify webhook URL is configured in client subscription +3. Check API Destination is routing to correct endpoint +4. Increase wait time (callbacks may take >5 seconds to appear) +5. Query CloudWatch Logs console directly to verify log entries exist + +### Permission errors querying CloudWatch + +Ensure your test execution role has these permissions: + +```json +{ + "Effect": "Allow", + "Action": ["logs:FilterLogEvents", "logs:DescribeLogStreams"], + "Resource": "arn:aws:logs:eu-west-2:*:log-group:/aws/lambda/nhs-notify-callbacks-*" +} +``` + +### Timeout waiting for callbacks + +- Increase timeout in test (default 30s may be insufficient in some environments) +- Check EventBridge Pipe is running and not throttled +- Verify Lambda transform does not error (check its CloudWatch logs) +- Check API Destination retry configuration + +## References + +- Mock webhook implementation: [lambdas/mock-webhook-lambda](../../lambdas/mock-webhook-lambda/) +- CloudWatch helper functions: [helpers/cloudwatch-helpers.ts](./helpers/cloudwatch-helpers.ts) +- Integration test examples: [event-bus-to-webhook.test.ts](./event-bus-to-webhook.test.ts) +- Pattern based on: `repos/comms-mgr/comms/components/comms-e2e-tests/tests/callbacks/assertions.ts` 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..790be23 --- /dev/null +++ b/tests/integration/event-bus-to-webhook.test.ts @@ -0,0 +1,413 @@ +/** + * Integration test for Event Bus to Webhook flow + * + * Tests the end-to-end flow: + * 1. Event published to Shared Event Bus + * 2. Consumed by SQS Queue + * 3. Processed by EventBridge Pipe + * 4. Transformed by Lambda + * 5. Routed to API Destination + * 6. Delivered to client webhook + * + * This test requires AWS infrastructure to be deployed. + * Run with: npm run test:integration + * + * ## Webhook Verification + * + * To verify webhook delivery, deploy the mock-webhook-lambda and configure: + * - TEST_WEBHOOK_URL: URL of the deployed mock webhook Lambda + * - TEST_WEBHOOK_LOG_GROUP: CloudWatch log group name (e.g., /aws/lambda/nhs-notify-callbacks-dev-mock-webhook) + * + * Then use helpers from ./helpers/cloudwatch-helpers to query received callbacks: + * + * ```typescript + * import { getMessageStatusCallbacks } from './helpers'; + * + * const callbacks = await getMessageStatusCallbacks( + * process.env.TEST_WEBHOOK_LOG_GROUP!, + * messageId + * ); + * + * expect(callbacks).toContainEqual( + * expect.objectContaining({ + * type: 'MessageStatus', + * attributes: expect.objectContaining({ + * messageId, + * messageStatus: 'delivered' + * }) + * }) + * ); + * ``` + */ + +import { + EventBridgeClient, + PutEventsCommand, + type PutEventsRequestEntry, +} from "@aws-sdk/client-eventbridge"; +import { + GetQueueAttributesCommand, + PurgeQueueCommand, + SQSClient, +} from "@aws-sdk/client-sqs"; +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"; + +// Skipped - unfinished +// 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 () => { + // Purge test queue before each test + 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) { + // Skip test if webhook URL not configured + return; + } + + const messageStatusEvent: StatusTransitionEvent = { + profileversion: "1.0.0", + profilepublished: "2025-10", + 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(), + recordedtime: new Date().toISOString(), + datacontenttype: "application/json", + dataschema: "https://nhs.uk/schemas/notify/message-status-data.v1.json", + severitynumber: 2, + severitytext: "INFO", + traceparent: "00-4d678967f96e353c07a0a31c1849b500-07f83ba58dd8df70-01", + data: { + "notify-payload": { + "notify-data": { + clientId: "test-client-integration", + 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(), + }, + }, + "notify-metadata": { + teamResponsible: "Team 1", + notifyDomain: "Delivering", + microservice: "integration-test", + repositoryUrl: + "https://github.com/NHSDigital/nhs-notify-client-callbacks", + accountId: "123456789012", + environment: "development", + instance: "primary", + microserviceInstanceId: "test-instance", + microserviceVersion: "1.0.0", + }, + }, + }, + }; + + // Publish event to Event Bus + const putEventsCommand = new PutEventsCommand({ + Entries: [ + { + EventBusName: TEST_EVENT_BUS_NAME, + Source: messageStatusEvent.source, + DetailType: messageStatusEvent.type, + Detail: JSON.stringify(messageStatusEvent), + Time: new Date(messageStatusEvent.time), + } as PutEventsRequestEntry, + ], + }); + + const putEventsResponse = await eventBridgeClient.send(putEventsCommand); + + // Verify event was accepted + expect(putEventsResponse.FailedEntryCount).toBe(0); + expect(putEventsResponse.Entries).toHaveLength(1); + expect(putEventsResponse.Entries![0].EventId).toBeDefined(); + + // Wait for event processing (Lambda execution, API Destination delivery) + await new Promise((resolve) => { + setTimeout(resolve, 5000); + }); + + // Verify queue metrics (optional - requires TEST_QUEUE_URL) + let queueMessageCount = 0; + if (TEST_QUEUE_URL) { + const queueAttributesCommand = new GetQueueAttributesCommand({ + QueueUrl: TEST_QUEUE_URL, + AttributeNames: [ + "ApproximateNumberOfMessages", + "ApproximateNumberOfMessagesNotVisible", + ], + }); + + const queueAttributes = await sqsClient.send(queueAttributesCommand); + queueMessageCount = Number( + queueAttributes.Attributes?.ApproximateNumberOfMessages || 0, + ); + } + + // Messages should have been processed (not visible anymore) + expect(TEST_QUEUE_URL ? queueMessageCount : 0).toBe(0); + + // Verify webhook delivery (optional - requires TEST_WEBHOOK_LOG_GROUP) + if (TEST_WEBHOOK_LOG_GROUP) { + const { getMessageStatusCallbacks } = await import( + "./helpers/index.js" + ); + const callbacks = await getMessageStatusCallbacks( + TEST_WEBHOOK_LOG_GROUP, + messageStatusEvent.data["notify-payload"]["notify-data"].messageId, + ); + // eslint-disable-next-line jest/no-conditional-expect + expect(callbacks).toHaveLength(1); + // eslint-disable-next-line jest/no-conditional-expect + expect(callbacks[0]).toMatchObject({ + type: "MessageStatus", + // eslint-disable-next-line jest/no-conditional-expect + attributes: expect.objectContaining({ + messageStatus: "delivered", + }), + }); + } + }, 30_000); // 30 second timeout for integration test + + it("should filter out events not matching client subscription", async () => { + if (!TEST_WEBHOOK_URL) { + // Skip test if webhook URL not configured + return; + } + + const messageStatusEvent: StatusTransitionEvent = { + profileversion: "1.0.0", + profilepublished: "2025-10", + 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(), + recordedtime: new Date().toISOString(), + datacontenttype: "application/json", + dataschema: "https://nhs.uk/schemas/notify/message-status-data.v1.json", + severitynumber: 2, + severitytext: "INFO", + traceparent: "00-4d678967f96e353c07a0a31c1849b500-07f83ba58dd8df70-01", + data: { + "notify-payload": { + "notify-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(), + }, + }, + "notify-metadata": { + teamResponsible: "Team 1", + notifyDomain: "Delivering", + microservice: "integration-test", + repositoryUrl: + "https://github.com/NHSDigital/nhs-notify-client-callbacks", + accountId: "123456789012", + environment: "development", + instance: "primary", + microserviceInstanceId: "test-instance", + microserviceVersion: "1.0.0", + }, + }, + }, + }; + + // Publish event to Event Bus + const putEventsCommand = new PutEventsCommand({ + Entries: [ + { + EventBusName: TEST_EVENT_BUS_NAME, + Source: messageStatusEvent.source, + DetailType: messageStatusEvent.type, + Detail: JSON.stringify(messageStatusEvent), + Time: new Date(messageStatusEvent.time), + } as PutEventsRequestEntry, + ], + }); + + const putEventsResponse = await eventBridgeClient.send(putEventsCommand); + + // Verify event was accepted + expect(putEventsResponse.FailedEntryCount).toBe(0); + + // Wait for event processing + await new Promise((resolve) => { + setTimeout(resolve, 5000); + }); + + // Event should be filtered out by Lambda and not delivered to webhook + // Manual verification: check CloudWatch logs show event was discarded with appropriate logging + }, 30_000); + }); + + describe("Channel Status Event Flow", () => { + it("should process channel status event from Event Bus to webhook", async () => { + if (!TEST_WEBHOOK_URL) { + // Skip test if webhook URL not configured + return; + } + + const channelStatusEvent: StatusTransitionEvent = { + profileversion: "1.0.0", + profilepublished: "2025-10", + 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(), + recordedtime: new Date().toISOString(), + datacontenttype: "application/json", + dataschema: "https://nhs.uk/schemas/notify/channel-status-data.v1.json", + severitynumber: 2, + severitytext: "INFO", + traceparent: "00-4d678967f96e353c07a0a31c1849b500-07f83ba58dd8df70-02", + data: { + "notify-payload": { + "notify-data": { + clientId: "test-client-integration", + messageId: `test-msg-${Date.now()}`, + messageReference: `test-ref-${Date.now()}`, + channel: "nhsapp", + channelStatus: "delivered", + channelStatusDescription: "Integration test channel delivered", + supplierStatus: "delivered", + cascadeType: "PREFERRED", + 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(), + }, + }, + "notify-metadata": { + teamResponsible: "Team 1", + notifyDomain: "Delivering", + microservice: "integration-test", + repositoryUrl: + "https://github.com/NHSDigital/nhs-notify-client-callbacks", + accountId: "123456789012", + environment: "development", + instance: "primary", + microserviceInstanceId: "test-instance", + microserviceVersion: "1.0.0", + }, + }, + }, + }; + + // Publish event to Event Bus + const putEventsCommand = new PutEventsCommand({ + Entries: [ + { + EventBusName: TEST_EVENT_BUS_NAME, + Source: channelStatusEvent.source, + DetailType: channelStatusEvent.type, + Detail: JSON.stringify(channelStatusEvent), + Time: new Date(channelStatusEvent.time), + } as PutEventsRequestEntry, + ], + }); + + const putEventsResponse = await eventBridgeClient.send(putEventsCommand); + + // Verify event was accepted + expect(putEventsResponse.FailedEntryCount).toBe(0); + expect(putEventsResponse.Entries).toHaveLength(1); + expect(putEventsResponse.Entries![0].EventId).toBeDefined(); + + // Wait for event processing + await new Promise((resolve) => { + setTimeout(resolve, 5000); + }); + + // Verify queue metrics (optional) + let queueMessageCount = 0; + if (TEST_QUEUE_URL) { + const queueAttributesCommand = new GetQueueAttributesCommand({ + QueueUrl: TEST_QUEUE_URL, + AttributeNames: ["ApproximateNumberOfMessages"], + }); + + const queueAttributes = await sqsClient.send(queueAttributesCommand); + queueMessageCount = Number( + queueAttributes.Attributes?.ApproximateNumberOfMessages || 0, + ); + } + + // Messages should have been processed + expect(TEST_QUEUE_URL ? queueMessageCount : 0).toBe(0); + }, 30_000); + }); +}); diff --git a/tests/integration/helpers/cloudwatch-helpers.ts b/tests/integration/helpers/cloudwatch-helpers.ts new file mode 100644 index 0000000..d785800 --- /dev/null +++ b/tests/integration/helpers/cloudwatch-helpers.ts @@ -0,0 +1,132 @@ +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" }); + +/** + * Query CloudWatch Logs for mock webhook callbacks + * + * @param logGroupName - CloudWatch log group name for the mock webhook lambda + * @param pattern - Filter pattern (e.g., messageId) + * @param startTime - Optional start time for log search (defaults to 5 minutes ago) + * @returns Array of log entries containing callback payloads + */ +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)] : [], + ); +} + +/** + * Parse callback payloads from CloudWatch log messages + * + * Extracts the JSON payload from log messages with format: + * "CALLBACK {messageId} {messageType} : {JSON payload}" + * + * @param logs - Array of log entries from CloudWatch + * @returns Array of parsed callback payloads + */ +export function parseCallbacksFromLogs(logs: unknown[]): CallbackPayload[] { + return logs + .map((log: unknown) => { + if ( + typeof log === "object" && + log !== null && + "msg" in log && + typeof log.msg === "string" + ) { + // Extract JSON from "CALLBACK {id} {type} : {json}" format + 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); +} + +/** + * Get message status 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 MessageStatus callback payloads + */ +export async function getMessageStatusCallbacks( + logGroupName: string, + requestItemId: string, + startTime?: Date, +): Promise { + const logs = await getCallbackLogsFromCloudWatch( + logGroupName, + `%${requestItemId}%MessageStatus%`, + startTime, + ); + return parseCallbacksFromLogs(logs); +} + +/** + * Get channel status 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 ChannelStatus callback payloads + */ +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/tsconfig.json b/tsconfig.json index ccb61df..7670de5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,9 @@ { "$schema": "https://json.schemastore.org/tsconfig", - "extends": "./tsconfig.base.json", "compilerOptions": { "noEmit": true }, + "extends": "./tsconfig.base.json", "include": [ "lambdas/*/src/**/*", "scripts/*/src/**/*",