From a1557f8b51c31126202d37f12db60baae64125b4 Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Tue, 30 Jul 2024 13:11:41 +0100 Subject: [PATCH 01/59] Bump nodejs puppeteer `runtime_version` --- .../modules/cloud-watch-canary/cloudwatch.synthetics-canary.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terraform/modules/cloud-watch-canary/cloudwatch.synthetics-canary.tf b/terraform/modules/cloud-watch-canary/cloudwatch.synthetics-canary.tf index d2ddceea..4322e44c 100644 --- a/terraform/modules/cloud-watch-canary/cloudwatch.synthetics-canary.tf +++ b/terraform/modules/cloud-watch-canary/cloudwatch.synthetics-canary.tf @@ -4,7 +4,7 @@ resource "aws_synthetics_canary" "this" { execution_role_arn = module.iam_canary_role.iam_role_arn zip_file = data.archive_file.canary_script.output_path handler = "index.handler" - runtime_version = "syn-nodejs-puppeteer-8.0" + runtime_version = "syn-nodejs-puppeteer-9.0" start_canary = true delete_lambda = true From d285678ca5822060c3b404169e3fca36b5d75095 Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Tue, 30 Jul 2024 13:12:14 +0100 Subject: [PATCH 02/59] Provide env vars to canary runtime from module variables --- .../cloud-watch-canary/cloudwatch.synthetics-canary.tf | 4 ++++ terraform/modules/cloud-watch-canary/vars.tf | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/terraform/modules/cloud-watch-canary/cloudwatch.synthetics-canary.tf b/terraform/modules/cloud-watch-canary/cloudwatch.synthetics-canary.tf index 4322e44c..6c780522 100644 --- a/terraform/modules/cloud-watch-canary/cloudwatch.synthetics-canary.tf +++ b/terraform/modules/cloud-watch-canary/cloudwatch.synthetics-canary.tf @@ -16,4 +16,8 @@ resource "aws_synthetics_canary" "this" { subnet_ids = var.subnet_ids security_group_ids = [module.canary_security_group.security_group_id] } + + run_config { + environment_variables = var.environment_variables + } } diff --git a/terraform/modules/cloud-watch-canary/vars.tf b/terraform/modules/cloud-watch-canary/vars.tf index db4e8afb..1504da22 100644 --- a/terraform/modules/cloud-watch-canary/vars.tf +++ b/terraform/modules/cloud-watch-canary/vars.tf @@ -37,4 +37,10 @@ variable "schedule_expression" { variable "script_path" { description = "The file path of the script to attach to the canary" type = string +} + +variable "environment_variables" { + description = "Map of environment variables to provide to the canary runtime." + type = map(string) + default = {} } \ No newline at end of file From aa38b4ab32e2a80105bb70e98a0691e3ab1dd09f Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Tue, 30 Jul 2024 13:12:43 +0100 Subject: [PATCH 03/59] Build zip file containing single source code file for canary script --- terraform/modules/cloud-watch-canary/script.tf | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/terraform/modules/cloud-watch-canary/script.tf b/terraform/modules/cloud-watch-canary/script.tf index 8b9cb9ac..4bb661c9 100644 --- a/terraform/modules/cloud-watch-canary/script.tf +++ b/terraform/modules/cloud-watch-canary/script.tf @@ -1,13 +1,14 @@ locals { - file_content = file(var.script_path) - zip = "builds/${var.name}-${sha256(local.file_content)}.zip" + script_content = file("${var.script_path}/index.js") + script_content_hash = sha256(local.script_content) + zip = "builds/${var.script_path}-${local.script_content_hash}.zip" } data "archive_file" "canary_script" { type = "zip" output_path = local.zip source { - content = local.file_content + content = local.script_content filename = "nodejs/node_modules/index.js" } -} \ No newline at end of file +} From 462b9eedbe6de34418bcc6ce517950e82a5a910d Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Tue, 30 Jul 2024 14:55:54 +0100 Subject: [PATCH 04/59] Add `timeout_in_seconds` variable to `cloud-watch-canary` module --- .../cloud-watch-canary/cloudwatch.synthetics-canary.tf | 1 + terraform/modules/cloud-watch-canary/vars.tf | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/terraform/modules/cloud-watch-canary/cloudwatch.synthetics-canary.tf b/terraform/modules/cloud-watch-canary/cloudwatch.synthetics-canary.tf index 6c780522..000e4c0e 100644 --- a/terraform/modules/cloud-watch-canary/cloudwatch.synthetics-canary.tf +++ b/terraform/modules/cloud-watch-canary/cloudwatch.synthetics-canary.tf @@ -18,6 +18,7 @@ resource "aws_synthetics_canary" "this" { } run_config { + timeout_in_seconds = var.timeout_in_seconds environment_variables = var.environment_variables } } diff --git a/terraform/modules/cloud-watch-canary/vars.tf b/terraform/modules/cloud-watch-canary/vars.tf index 1504da22..5b96d3f7 100644 --- a/terraform/modules/cloud-watch-canary/vars.tf +++ b/terraform/modules/cloud-watch-canary/vars.tf @@ -34,6 +34,11 @@ variable "schedule_expression" { type = string } +variable "timeout_in_seconds" { + description = "The number of seconds which the canary should run until timing out." + type = number +} + variable "script_path" { description = "The file path of the script to attach to the canary" type = string From 252eb68c32098dd844716bf9452dd971b24444f2 Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Tue, 30 Jul 2024 14:56:45 +0100 Subject: [PATCH 05/59] Implement canary to traverse frontend pages c/w screenshots --- src/canary-front-end-screenshots/index.js | 130 ++++++++++++++++++ .../20-app/cloudwatch.canary.front-end.tf | 21 +++ 2 files changed, 151 insertions(+) create mode 100644 src/canary-front-end-screenshots/index.js create mode 100644 terraform/20-app/cloudwatch.canary.front-end.tf diff --git a/src/canary-front-end-screenshots/index.js b/src/canary-front-end-screenshots/index.js new file mode 100644 index 00000000..e12043b9 --- /dev/null +++ b/src/canary-front-end-screenshots/index.js @@ -0,0 +1,130 @@ +const {URL} = require('url'); +const synthetics = require('Synthetics'); +const log = require('SyntheticsLogger'); +const syntheticsConfiguration = synthetics.getConfiguration(); +const syntheticsLogHelper = require('SyntheticsLogHelper'); + +function extractUrlsFromSitemap(xml) { + const urlRegex = /(.*?)<\/loc>/g; + const urls = []; + let match; + + while ((match = urlRegex.exec(xml)) !== null) { + urls.push(match[1].trim()); + } + return urls; +} + +async function parseSitemap(url) { + const response = await fetch(url); + const xmlString = await response.text(); + return extractUrlsFromSitemap(xmlString) +} + +async function fetchAndParseSitemap(url) { + try { + return await parseSitemap(url) + } catch (error) { + log.error("Error fetching or parsing XML:", error); + } +} + +const loadBlueprint = async function () { + const urls = await fetchAndParseSitemap(process.env.SITEMAP_URL) + + // Set screenshot option + const takeScreenshot = true; + + /* Disabling default step screen shots taken during Synthetics.executeStep() calls + * Step will be used to publish metrics on time taken to load dom content but + * Screenshots will be taken outside the executeStep to allow for page to completely load with domcontentloaded + * You can change it to load, networkidle0, networkidle2 depending on what works best for you. + */ + syntheticsConfiguration.disableStepScreenshots(); + syntheticsConfiguration.setConfig({ + continueOnStepFailure: true, + includeRequestHeaders: true, // Enable if headers should be displayed in HAR + includeResponseHeaders: true, // Enable if headers should be displayed in HAR + restrictedHeaders: [], // Value of these headers will be redacted from logs and reports + restrictedUrlParameters: [] // Values of these url parameters will be redacted from logs and reports + + }); + + let page = await synthetics.getPage(); + for (const url of urls) { + await loadUrl(page, url, takeScreenshot); + } +}; + +const resetPage = async function (page) { + try { + await page.goto('about:blank', {waitUntil: ['load', 'networkidle0'], timeout: 30000}); + } catch (e) { + synthetics.addExecutionError('Unable to open a blank page. ', e); + } +} + +const loadUrl = async function (page, url, takeScreenshot) { + let stepName = null; + let domcontentloaded = false; + + try { + stepName = new URL(url).hostname; + } catch (e) { + const errorString = `Error parsing url: ${url}. ${e}`; + log.error(errorString); + /* If we fail to parse the URL, don't emit a metric with a stepName based on it. + It may not be a legal CloudWatch metric dimension name and we may not have an alarms + setup on the malformed URL stepName. Instead, fail this step which will + show up in the logs and will fail the overall canary and alarm on the overall canary + success rate. + */ + throw e; + } + + await synthetics.executeStep(stepName, async function () { + const sanitizedUrl = syntheticsLogHelper.getSanitizedUrl(url); + + /* You can customize the wait condition here. For instance, using 'networkidle2' or 'networkidle0' to load page completely. + networkidle0: Navigation is successful when the page has had no network requests for half a second. This might never happen if page is constantly loading multiple resources. + networkidle2: Navigation is successful when the page has no more then 2 network requests for half a second. + domcontentloaded: It's fired as soon as the page DOM has been loaded, without waiting for resources to finish loading. If needed add explicit wait with await new Promise(r => setTimeout(r, milliseconds)) + */ + const response = await page.goto(url, {waitUntil: ['domcontentloaded'], timeout: 30000}); + log.info("response: ", JSON.stringify(response)) + + if (response) { + domcontentloaded = true; + const status = response.status(); + const statusText = response.statusText(); + + logResponseString = `Response from url: ${sanitizedUrl} Status: ${status} Status Text: ${statusText}`; + + //If the response status code is not a 2xx success code + if (response.status() < 200 || response.status() > 299) { + throw new Error(`Failed to load url: ${sanitizedUrl} ${response.status()} ${response.statusText()}`); + } + } else { + const logNoResponseString = `No response returned for url: ${sanitizedUrl}`; + log.error(logNoResponseString); + throw new Error(logNoResponseString); + } + }); + + // Wait for 3 seconds to let page load fully before taking screenshot. + if (domcontentloaded && takeScreenshot) { + await new Promise(r => setTimeout(r, 3000)); + await synthetics.takeScreenshot(stepName, 'loaded'); + } + + // Reset page + await resetPage(page); +}; + +async function handler() { + return await loadBlueprint(); +} + +module.exports = { + handler +} \ No newline at end of file diff --git a/terraform/20-app/cloudwatch.canary.front-end.tf b/terraform/20-app/cloudwatch.canary.front-end.tf new file mode 100644 index 00000000..5bfcf107 --- /dev/null +++ b/terraform/20-app/cloudwatch.canary.front-end.tf @@ -0,0 +1,21 @@ +module "cloudwatch_canary_front_end_screenshots" { + source = "../modules/cloud-watch-canary" + create = true + name = "${local.prefix}-display" + s3_access_logs_id = data.aws_s3_bucket.s3_access_logs.id + s3_logs_destination = { + bucket_id = module.s3_canary_logs.s3_bucket_id + bucket_arn = module.s3_canary_logs.s3_bucket_arn + } + + vpc_id = module.vpc.vpc_id + subnet_ids = module.vpc.private_subnets + + schedule_expression = "rate(10 minutes)" + timeout_in_seconds = 600 + script_path = "../../src/canary-front-end-screenshots" + + environment_variables = { + SITEMAP_URL = "${local.urls.front_end}/sitemap.xml" + } +} From ad227bce5bba8619f263bba3fe858190bd9d603f Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Tue, 30 Jul 2024 15:44:18 +0100 Subject: [PATCH 06/59] Set retention periods for canary runs --- .../modules/cloud-watch-canary/cloudwatch.synthetics-canary.tf | 3 +++ 1 file changed, 3 insertions(+) diff --git a/terraform/modules/cloud-watch-canary/cloudwatch.synthetics-canary.tf b/terraform/modules/cloud-watch-canary/cloudwatch.synthetics-canary.tf index 000e4c0e..8e690b9c 100644 --- a/terraform/modules/cloud-watch-canary/cloudwatch.synthetics-canary.tf +++ b/terraform/modules/cloud-watch-canary/cloudwatch.synthetics-canary.tf @@ -21,4 +21,7 @@ resource "aws_synthetics_canary" "this" { timeout_in_seconds = var.timeout_in_seconds environment_variables = var.environment_variables } + + success_retention_period = 7 + failure_retention_period = 31 } From 6aa07c629ab48b3a3bb08009f59d92e23098ef0d Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Mon, 5 Aug 2024 14:07:24 +0100 Subject: [PATCH 07/59] Bump `aws` provider from `v5.58.0` to `v5.60.0` --- terraform/20-app/.terraform.lock.hcl | 25 +++++++++++++------------ terraform/20-app/versions.tf | 2 +- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/terraform/20-app/.terraform.lock.hcl b/terraform/20-app/.terraform.lock.hcl index 65b73b61..26c4dab7 100644 --- a/terraform/20-app/.terraform.lock.hcl +++ b/terraform/20-app/.terraform.lock.hcl @@ -23,19 +23,20 @@ provider "registry.terraform.io/hashicorp/archive" { } provider "registry.terraform.io/hashicorp/aws" { - version = "5.58.0" - constraints = ">= 3.29.0, >= 3.74.0, >= 4.0.0, >= 4.66.1, >= 5.0.0, >= 5.12.0, >= 5.25.0, >= 5.27.0, >= 5.30.0, >= 5.32.0, >= 5.37.0, >= 5.46.0, >= 5.49.0, >= 5.53.0, >= 5.58.0, 5.58.0" + version = "5.60.0" + constraints = ">= 3.29.0, >= 3.74.0, >= 4.0.0, >= 4.66.1, >= 5.0.0, >= 5.12.0, >= 5.25.0, >= 5.27.0, >= 5.30.0, >= 5.32.0, >= 5.37.0, >= 5.46.0, >= 5.49.0, >= 5.53.0, >= 5.58.0, 5.60.0" hashes = [ - "h1:6vsFc7SmmlElqg3k0X6azrO0yarM7UPCUF4XsAYryjA=", - "h1:XnAwb/MGeP7sxz/0SKLQF1ujaP7Bg15ol+ca7KZruio=", - "h1:dMyVBj7KKMblfLn6+aIP37jK/9HwfMtX9TJvzMjmz2s=", - "zh:15e9be54a8febe8e560362b10967cb60b680ca3f78fe207d7209b76e076f59d3", - "zh:240f6899a2cec259aa2729ce031f6af2b453f90a8b59118bb2571c54acc65db8", - "zh:2b6e8e2ab1a3dce1001503dba6086a128bb2a71652b0d0b3b107db665b7d6881", - "zh:579b0ed95247a0bd8bfb3fac7fb767547dde76026c578f4f184b5743af5e32cc", - "zh:6adcd10fd12be0be9eb78a89e745a5b77ae0d8b3522cd782456a71178aad8ccb", - "zh:7f829cef82f0a02faa97d0fbe1417a40b73fc5142e883b12eebc5b71015efac9", - "zh:81977f001998c9096f7b59710996e159774a9313c1bc03db3beb81c3e016ebef", + "h1:Ou/WgUdyL4dSzfO1U5J0Z7i4FS4QRBVxqVWFFnRl0Fk=", + "h1:msnFtzhM9fQgi5ePG7Skt5DvnqOiWqMSxCNBred/hso=", + "h1:p9+40kdklLTJLQ/y7wxNjuKxUK8AVB4L9424NGNK4rY=", + "zh:08f49c9eb865e136a55dda3eb2b790f6d55cdac49f6638391dbea4b865cf307b", + "zh:090dd8b40ebf0f8e9ea05b9a142add9caeb7988d3d96c5c112e8c67c0edf566f", + "zh:30f336af1b4f0824fce2cc6e81af0986b325b135436c9d892d081e435aeed67e", + "zh:338195ca3b41249874110253412d8913f770c22294af05799ea1e343050906f5", + "zh:3a8a45b17750b01192a0fbeeed0d05c2c04840344d78d5e3233b3ecbeec17a1c", + "zh:486efe72d39f0736d9b7e00e5b889288264458a57aa0cff2d75688d6db372ee5", + "zh:5fdccc448a085fea8ecfae43ae326840abfcdf1a0aa8b8c79dd466392aa5cc3a", + "zh:9521639755cd07ec7efde86a534770e436e16a93692d070a00f6419c1038d59c", "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", "zh:a5d98ac6fab6e6c85164ca7dd38f94a1e44bd70c0e8354c61f7fbabf698957cd", "zh:c27fa4fed50f6f83ca911bef04f05d635a7b7a01a89dc8fc5d66a277588f08df", diff --git a/terraform/20-app/versions.tf b/terraform/20-app/versions.tf index e0f7116a..b71afbee 100644 --- a/terraform/20-app/versions.tf +++ b/terraform/20-app/versions.tf @@ -2,7 +2,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = "5.58.0" + version = "5.60.0" } random = { source = "hashicorp/random" From aaefdd2d21e2663f6b195e06cca75a0b5a98ac61 Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Mon, 5 Aug 2024 14:17:21 +0100 Subject: [PATCH 08/59] Add `slack_token` and `slack_channel_id` to slack secret --- terraform/20-app/secret-manager.tf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/terraform/20-app/secret-manager.tf b/terraform/20-app/secret-manager.tf index 3d90c033..bfd97dd3 100644 --- a/terraform/20-app/secret-manager.tf +++ b/terraform/20-app/secret-manager.tf @@ -195,5 +195,7 @@ resource "aws_secretsmanager_secret_version" "slack_webhook_url" { secret_id = aws_secretsmanager_secret.slack_webhook_url.id secret_string = jsonencode({ slack_webhook_url = "" + slack_token = "" + slack_channel_id = "" }) } From a0cc09b44201273e02cf6024295aeb4ece60354f Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Mon, 5 Aug 2024 14:18:08 +0100 Subject: [PATCH 09/59] Add dedicated SNS topic to `cloud-watch-canary` --- terraform/modules/cloud-watch-canary/sns.alarm.tf | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 terraform/modules/cloud-watch-canary/sns.alarm.tf diff --git a/terraform/modules/cloud-watch-canary/sns.alarm.tf b/terraform/modules/cloud-watch-canary/sns.alarm.tf new file mode 100644 index 00000000..73852374 --- /dev/null +++ b/terraform/modules/cloud-watch-canary/sns.alarm.tf @@ -0,0 +1,11 @@ +module "sns_topic_alarm" { + source = "terraform-aws-modules/sns/aws" + name = "${var.name}-alarms" + + subscriptions = { + lambda = { + protocol = "lambda" + endpoint = var.lambda_function_notification_arn + } + } +} \ No newline at end of file From 51e9f64e1c03cb2ad7611f7f55e6c267487dd924 Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Mon, 5 Aug 2024 14:22:24 +0100 Subject: [PATCH 10/59] Bump `archive` provider --- terraform/20-app/.terraform.lock.hcl | 43 ++++++++++++++-------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/terraform/20-app/.terraform.lock.hcl b/terraform/20-app/.terraform.lock.hcl index 26c4dab7..4579666e 100644 --- a/terraform/20-app/.terraform.lock.hcl +++ b/terraform/20-app/.terraform.lock.hcl @@ -2,23 +2,23 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/archive" { - version = "2.4.2" + version = "2.5.0" hashes = [ - "h1:1eOz9vM/55vnQjxk23RhnYga7PZq8n2rGxG+2Vx2s6w=", - "h1:G4v6F6Lhqlo3EKGBKEK/kJRhNcQiRrhEdUiVpBHKHOA=", - "h1:WfIjVbYA9s/uN2FwhGoiffT7CLFydy7MT1waFbt9YrY=", - "zh:08faed7c9f42d82bc3d406d0d9d4971e2d1c2d34eae268ad211b8aca57b7f758", - "zh:3564112ed2d097d7e0672378044a69b06642c326f6f1584d81c7cdd32ebf3a08", - "zh:53cd9afd223c15828c1916e68cb728d2be1cbccb9545568d6c2b122d0bac5102", - "zh:5ae4e41e3a1ce9d40b6458218a85bbde44f21723943982bca4a3b8bb7c103670", - "zh:5b65499218b315b96e95c5d3463ea6d7c66245b59461217c99eaa1611891cd2c", + "h1:GyV//bFbFWEll/6XafMvSUCmJL+r7wDDz222dMEmV3c=", + "h1:HXf8h8Z4JYEkBND/JiqC+CjluKqifKoDGrL1IsRo15M=", + "h1:OTk41JfiDc1TVFTcRZ//4+jwPBIcWHXOwN29mjdOyug=", + "zh:3b5774d20e87058d6d67d9ad4ce3fc4a5f7ea7748d345fa6721e24a0cbb0a3d4", + "zh:3b94e706ac0f5151880ccc9e63d33c4113361f27e64224a942caa04a5a19cd44", "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:7f45b35a8330bebd184c2545a41782ff58240ed6ba947274d9881dd5da44b02e", - "zh:87e67891033214e55cfead1391d68e6a3bf37993b7607753237e82aa3250bb71", - "zh:de3590d14037ad81fc5cedf7cfa44614a92452d7b39676289b704a962050bc5e", - "zh:e7e6f2ea567f2dbb3baa81c6203be69f9cd6aeeb01204fd93e3cf181e099b610", - "zh:fd24d03c89a7702628c2e5a3c732c0dede56fa75a08da4a1efe17b5f881c88e2", - "zh:febf4b7b5f3ff2adff0573ef6361f09b6638105111644bdebc0e4f575373935f", + "zh:7d7201858fa9376029818c9d017b4b53a933cea75480306b1122663d1e8eea2b", + "zh:8c8c7537978adf12271fe143f93b3587bb5dbabf8202ff49d0e3955b7bddc24b", + "zh:a5942584665a2689e73f3a3c43296adeaeb7e8698631d157419aa931ff856907", + "zh:a63673abdba624d60c84b819184fe86422bdbdf6bc73f68d903a7191aed32c00", + "zh:bcd1586cc32b263265e09e78f56dba3a6b6b19f5371c099a9d7a1bfe0b0667cc", + "zh:cc9e70e186e4dcef60208b4a64b42e6813b197e21ea106a96bb4eb23b54c3e44", + "zh:d4c8a0f69412892507a2c9ec0e334bcc2812a54b81212420d4f2c96ef58f713a", + "zh:e91e6d90bbc15252310eca6400d4188b29260aab0539480a3fc7b45e4d19c446", + "zh:fc468449c0dbda56aae6cb924e4a67578d18504b5b06e8989783182c6b4a5f73", ] } @@ -38,13 +38,12 @@ provider "registry.terraform.io/hashicorp/aws" { "zh:5fdccc448a085fea8ecfae43ae326840abfcdf1a0aa8b8c79dd466392aa5cc3a", "zh:9521639755cd07ec7efde86a534770e436e16a93692d070a00f6419c1038d59c", "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:a5d98ac6fab6e6c85164ca7dd38f94a1e44bd70c0e8354c61f7fbabf698957cd", - "zh:c27fa4fed50f6f83ca911bef04f05d635a7b7a01a89dc8fc5d66a277588f08df", - "zh:d4042bdf86ca6dc10e0cca91c4fcc592b12572d26185b3d37bbbb9e2026ac68b", - "zh:d536482cf4ace0d49a2a86c931150921649beae59337d0c02a785879fe943cf3", - "zh:e205f8243274a621fb9ef2b5e2c71e84c1670be1d23697739439f5a831fa620f", - "zh:eb76ce0c77fd76c47f57122c91c4fcf0f72c01423538ed7833eaa7eeaae2edf6", - "zh:ffe04e494af6cc7348ceb8d85f4c1d5a847a44510827b4496513c810a4d9196d", + "zh:c2fb9240a069da9f51e7379e76c3dfaad15a97430c2e32708a7d18345434e310", + "zh:daba836b89537dfa72bb8c77e88850c20fda2a3d0f5b3803cd3d6da0ce283e3e", + "zh:db7e0755ed120ed8311f6663f49aa7157da5072b906727db3a6c47d64e0b82c6", + "zh:ea5e3fca5197639c4ad1415ca96de2924a351ecd1a885dd9184843d5eec18dbb", + "zh:f3f322951d311e45a47361f24790a90a0b8ba6d3829a00c4066a361960d2ecef", + "zh:f48b44f4887d4b51a1406057f15f1e2161cb02b271b2659349958904c678e91c", ] } From ffd5b64b152eb3f7e879159f495aefd7a53d78f8 Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Mon, 5 Aug 2024 14:23:11 +0100 Subject: [PATCH 11/59] Implement required outputs from `cloud-watch-canary` module --- terraform/modules/cloud-watch-canary/outputs.tf | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 terraform/modules/cloud-watch-canary/outputs.tf diff --git a/terraform/modules/cloud-watch-canary/outputs.tf b/terraform/modules/cloud-watch-canary/outputs.tf new file mode 100644 index 00000000..dc4e4a19 --- /dev/null +++ b/terraform/modules/cloud-watch-canary/outputs.tf @@ -0,0 +1,11 @@ +output "sns_topic_arn" { + value = module.sns_topic_alarm.topic_arn +} + +output "name" { + value = var.name +} + +output "artifact_s3_location" { + value = aws_synthetics_canary.this.artifact_s3_location +} \ No newline at end of file From 9dffaf78a83762b8a2a3d33514c851f4cda045ce Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Mon, 5 Aug 2024 14:23:45 +0100 Subject: [PATCH 12/59] Add requirement for `lambda_function_notification_arn` variable to trigger notification process for failed canaries --- terraform/modules/cloud-watch-canary/vars.tf | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/terraform/modules/cloud-watch-canary/vars.tf b/terraform/modules/cloud-watch-canary/vars.tf index 5b96d3f7..6f9aa299 100644 --- a/terraform/modules/cloud-watch-canary/vars.tf +++ b/terraform/modules/cloud-watch-canary/vars.tf @@ -48,4 +48,9 @@ variable "environment_variables" { description = "Map of environment variables to provide to the canary runtime." type = map(string) default = {} -} \ No newline at end of file +} + +variable "lambda_function_notification_arn" { + description = "The ARN associated with Lambda function used to perform the notification trigger" + type = string +} From ad0fd5c457c8d33a95de2bf417d98ea62230c69d Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Mon, 5 Aug 2024 14:24:26 +0100 Subject: [PATCH 13/59] Point `script_path` at broken links canary script --- terraform/20-app/cloudwatch.canary.front-end.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terraform/20-app/cloudwatch.canary.front-end.tf b/terraform/20-app/cloudwatch.canary.front-end.tf index 5bfcf107..e236f065 100644 --- a/terraform/20-app/cloudwatch.canary.front-end.tf +++ b/terraform/20-app/cloudwatch.canary.front-end.tf @@ -13,7 +13,7 @@ module "cloudwatch_canary_front_end_screenshots" { schedule_expression = "rate(10 minutes)" timeout_in_seconds = 600 - script_path = "../../src/canary-front-end-screenshots" + script_path = "../../src/canary-front-end-broken-links" environment_variables = { SITEMAP_URL = "${local.urls.front_end}/sitemap.xml" From 5e930d31fe333b12d9bfd01ca6cc096b4cbba08c Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Mon, 5 Aug 2024 14:24:41 +0100 Subject: [PATCH 14/59] Formatting --- terraform/20-app/cloudwatch.canary.front-end.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/terraform/20-app/cloudwatch.canary.front-end.tf b/terraform/20-app/cloudwatch.canary.front-end.tf index e236f065..696043fc 100644 --- a/terraform/20-app/cloudwatch.canary.front-end.tf +++ b/terraform/20-app/cloudwatch.canary.front-end.tf @@ -11,8 +11,8 @@ module "cloudwatch_canary_front_end_screenshots" { vpc_id = module.vpc.vpc_id subnet_ids = module.vpc.private_subnets - schedule_expression = "rate(10 minutes)" - timeout_in_seconds = 600 + schedule_expression = "rate(10 minutes)" + timeout_in_seconds = 600 script_path = "../../src/canary-front-end-broken-links" environment_variables = { From 49779f296fcd0888e51d4331814b1d0326877cd0 Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Mon, 5 Aug 2024 14:25:30 +0100 Subject: [PATCH 15/59] Send lambda function ARN to `cloud-watch-canary` module so notifications can be triggered from failed canaries --- terraform/20-app/cloudwatch.canary.front-end.tf | 1 + 1 file changed, 1 insertion(+) diff --git a/terraform/20-app/cloudwatch.canary.front-end.tf b/terraform/20-app/cloudwatch.canary.front-end.tf index 696043fc..3027c2a2 100644 --- a/terraform/20-app/cloudwatch.canary.front-end.tf +++ b/terraform/20-app/cloudwatch.canary.front-end.tf @@ -14,6 +14,7 @@ module "cloudwatch_canary_front_end_screenshots" { schedule_expression = "rate(10 minutes)" timeout_in_seconds = 600 script_path = "../../src/canary-front-end-broken-links" + lambda_function_notification_arn = module.lambda_canary_notification.lambda_function_arn environment_variables = { SITEMAP_URL = "${local.urls.front_end}/sitemap.xml" From be6f121f06a7ab69d48958c0413dd48357d8f600 Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Mon, 5 Aug 2024 14:26:19 +0100 Subject: [PATCH 16/59] Add canary script used to crawl site and take snapshots of broken pages --- src/canary-front-end-broken-links/index.js | 270 +++++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100644 src/canary-front-end-broken-links/index.js diff --git a/src/canary-front-end-broken-links/index.js b/src/canary-front-end-broken-links/index.js new file mode 100644 index 00000000..5605dda2 --- /dev/null +++ b/src/canary-front-end-broken-links/index.js @@ -0,0 +1,270 @@ +const synthetics = require('Synthetics'); +const log = require('SyntheticsLogger'); +const BrokenLinkCheckerReport = require('BrokenLinkCheckerReport'); +const SyntheticsLink = require('SyntheticsLink'); +const syntheticsLogHelper = require('SyntheticsLogHelper'); +const syntheticsConfiguration = synthetics.getConfiguration(); + +function extractUrlsFromSitemap(xml) { + const urlRegex = /(.*?)<\/loc>/g; + const urls = []; + let match; + + while ((match = urlRegex.exec(xml)) !== null) { + urls.push(match[1].trim()); + } + return urls; +} + +async function parseSitemap(url) { + const response = await fetch(url); + const xmlString = await response.text(); + return extractUrlsFromSitemap(xmlString) +} + +async function fetchAndParseSitemap(url) { + try { + return await parseSitemap(url) + } catch (error) { + log.error("Error fetching or parsing XML:", error); + } +} + +// maximum number of links that would be followed +const limit = null; + +// Captures source page annotated screenshot for each link followed on a page. +const captureSourcePageScreenshot = true; + +// Captures destination page screenshot after loading a link successfully. +const captureDestinationPageScreenshotOnSuccess = false; + +// Captures destination page screenshot for broken links only. Note that links which do not return response have no destination screenshots. +const captureDestinationPageScreenshotOnFailure = true; + +// Close and Re-launch browser after checking these many links. This clears up /tmp disk storage occupied by chromium and launches a new browser for next set of links. +// Increase or decrease based on complexity of your website. +const numOfLinksToReLaunchBrowser = 20; + +// async function used to grab urls from page +// fetch hrefs from DOM +const grabLinks = async function (page, sourceUrl, exploredUrls) { + let grabbedLinks = []; + + const jsHandle = await page.evaluateHandle(() => { + return document.getElementsByTagName('a'); + }); + + const numberOfLinks = await page.evaluate(e => e.length, jsHandle); + + for (let i = 0; i < numberOfLinks; i++) { + let element = await page.evaluate((jsHandle, i, captureSourcePageScreenshot, exploredUrls) => { + let element = jsHandle[i]; + let url = String(element.href).trim(); + // Condition for grabbing a link + if (url != null && url.length > 0 && !exploredUrls.includes(url) && (url.startsWith('http') || url.startsWith('https'))) { + let text = element.text ? element.text.trim() : ''; + let originalBorderProp = element.style.border; + // Annotate this anchor element for source page screenshot. + if (captureSourcePageScreenshot) { + // Use color of your choosing for annotation. + element.style.border = '3px solid #e67e22'; + element.scrollIntoViewIfNeeded(); + } + return {text, url, originalBorderProp}; + } + }, jsHandle, i, captureSourcePageScreenshot, exploredUrls); + + if (element) { + let url = element.url; + let originalBorderProp = element.originalBorderProp; + exploredUrls.push(url); + + let sourcePageScreenshotResult; + if (captureSourcePageScreenshot) { + sourcePageScreenshotResult = await takeScreenshot(getFileName(url), "sourcePage"); + + // Reset css to original + await page.evaluate((jsHandle, i, originalBorderProp) => { + let element = jsHandle[i]; + element.style.border = originalBorderProp; + }, jsHandle, i, originalBorderProp); + } + + let link = new SyntheticsLink(url).withParentUrl(sourceUrl).withText(element.text); + link.addScreenshotResult(sourcePageScreenshotResult); + grabbedLinks.push(link); + + if (exploredUrls.length >= limit) { + break; + } + } + } + return grabbedLinks; +} + +// Take synthetics screenshot +const takeScreenshot = async function (fileName, suffix) { + try { + return await synthetics.takeScreenshot(fileName, suffix); + } catch (e) { + synthetics.addExecutionError('Unable to capture screenshot.', e); + } +} + +// Get the fileName for the screenshot based on the URI +const getFileName = function (url, defaultName = 'loaded') { + if (!url) return defaultName; + + const uri = new URL(url); + const pathname = uri.pathname.replace(/\/$/, ''); //remove trailing '/' + const fileName = !!pathname ? pathname.split('/').pop() : 'index'; + + // Remove characters which can't be used in S3 + return fileName.replace(/[^a-zA-Z0-9-_.!*'()]+/g, ''); +} + +// Broken link checker blueprint just uses one page to test availability of several urls +// Reset the page in-between to force a network event in case of a single page app +const resetPage = async function (page) { + try { + await page.goto('about:blank', {waitUntil: ['load', 'networkidle0'], timeout: 30000}); + } catch (e) { + synthetics.addExecutionError('Unable to open a blank page ', e); + } +} + +const webCrawlerBlueprint = async function () { + const urls = await fetchAndParseSitemap(process.env.SITEMAP_URL); + const exploredUrls = urls.slice(); + let synLinks = []; + let count = 0; + + let canaryError = null; + let brokenLinkError = null; + + let brokenLinkCheckerReport = new BrokenLinkCheckerReport(); + + syntheticsConfiguration.setConfig({ + includeRequestHeaders: true, // Enable if headers should be displayed in HAR + includeResponseHeaders: true, // Enable if headers should be displayed in HAR + restrictedHeaders: [], // Value of these headers will be redacted from logs and reports + restrictedUrlParameters: [] // Values of these url parameters will be redacted from logs and reports + }); + + + // Synthetics Puppeteer page instance + let page = await synthetics.getPage(); + + exploredUrls.forEach(url => { + synLinks.push(new SyntheticsLink(url)); + }); + + while (synLinks.length > 0) { + let link = synLinks.shift(); + let nav_url = link.getUrl(); + let sanitized_url = syntheticsLogHelper.getSanitizedUrl(nav_url); + link.withUrl(sanitized_url); + let fileName = getFileName(sanitized_url); + let response = null; + + count++; + + log.info("Current count: " + count + " Checking URL: " + sanitized_url); + + if (count % numOfLinksToReLaunchBrowser === 0 && count !== limit) { + log.info("Closing current browser and launching new"); + + // Close browser and stops HAR logging. + await synthetics.close(); + + // Launches a new browser and start HAR logging. + await synthetics.launch(); + + page = await synthetics.getPage(); + } else if (count !== 1) { + await resetPage(page); + } + + try { + /* You can customize the wait condition here. For instance, using 'networkidle2' may be less restrictive. + networkidle0: Navigation is successful when the page has had no network requests for half a second. This might never happen if page is constantly loading multiple resources. + networkidle2: Navigation is successful when the page has no more then 2 network requests for half a second. + domcontentloaded: It's fired as soon as the page DOM has been loaded, without waiting for resources to finish loading. If needed add explicit wait with await new Promise(r => setTimeout(r, milliseconds)) + */ + + response = await page.goto(nav_url, {waitUntil: ['load', 'networkidle0'], timeout: 30000}); + if (!response) { + brokenLinkError = "Failed to receive network response for url: " + sanitized_url; + log.error(brokenLinkError); + link = link.withFailureReason('Received null or undefined response.'); + } + } catch (e) { + brokenLinkError = "Failed to load url: " + sanitized_url + ". " + e; + log.error(brokenLinkError); + link = link.withFailureReason(e.toString()); + } + + if (response && response.status() && response.status() < 400) { + link = link.withStatusCode(response.status()).withStatusText(response.statusText()); + if (captureDestinationPageScreenshotOnSuccess) { + let screenshotResult = await takeScreenshot(fileName, 'succeeded'); + link.addScreenshotResult(screenshotResult); + } + } else if (response) { // Received 400s or 500s + const statusString = "Status code: " + response.status() + " " + response.statusText(); + brokenLinkError = "Failed to load url: " + sanitized_url + ". " + statusString; + log.info(brokenLinkError); + + link = link.withStatusCode(response.status()).withStatusText(response.statusText()).withFailureReason(statusString); + + if (captureDestinationPageScreenshotOnFailure) { + let screenshotResult = await takeScreenshot(fileName, 'failed'); + link.addScreenshotResult(screenshotResult); + } + } + + try { + // Adds this link to broken link checker report. Link with status code >= 400 is considered broken. Use addLink(link, isBrokenLink) to override this default behavior. + brokenLinkCheckerReport.addLink(link); + } catch (e) { + synthetics.addExecutionError('Unable to add link to broken link checker report.', e); + } + + // If current link was successfully loaded, grab more hyperlinks from this page. + if (response && response.status() && response.status() < 400 && exploredUrls.length < limit) { + try { + let moreLinks = await grabLinks(page, sanitized_url, exploredUrls); + if (moreLinks && moreLinks.length > 0) { + synLinks = synLinks.concat(moreLinks); + } + } catch (e) { + canaryError = "Unable to grab urls on page: " + sanitized_url + ". " + e; + log.error(canaryError); + } + } + } + + try { + synthetics.addReport(brokenLinkCheckerReport); + } catch (e) { + synthetics.addExecutionError('Unable to add broken link checker report.', e); + } + + log.info("Total links checked: " + brokenLinkCheckerReport.getTotalLinksChecked()); + + // Fail canary if 1 or more broken links found. + if (brokenLinkCheckerReport.getTotalBrokenLinks() !== 0) { + brokenLinkError = brokenLinkCheckerReport.getTotalBrokenLinks() + " broken link(s) detected. " + brokenLinkError; + log.error(brokenLinkError); + canaryError = canaryError ? (brokenLinkError + " " + canaryError) : brokenLinkError; + } + + if (canaryError) { + throw new Error(canaryError); + } +}; + +exports.handler = async () => { + return await webCrawlerBlueprint(); +}; From f0b024e080e3ef8b11ff2b1885dc120fe6292392 Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Mon, 5 Aug 2024 14:29:58 +0100 Subject: [PATCH 17/59] Add corresponding Cloudwatch alarm to be raised when canary run fails --- .../cloud-watch-canary/cloudwatch.alarm.tf | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 terraform/modules/cloud-watch-canary/cloudwatch.alarm.tf diff --git a/terraform/modules/cloud-watch-canary/cloudwatch.alarm.tf b/terraform/modules/cloud-watch-canary/cloudwatch.alarm.tf new file mode 100644 index 00000000..5ad6ba66 --- /dev/null +++ b/terraform/modules/cloud-watch-canary/cloudwatch.alarm.tf @@ -0,0 +1,23 @@ +module "cloudwatch_alarm" { + source = "terraform-aws-modules/cloudwatch/aws//modules/metric-alarm" + version = "5.3.1" + + alarm_name = "${var.name}-failed-alarm" + alarm_description = jsonencode( + { + "description": "Alarm for SNS", + "s3_prefix": "canary/eu-west-2/${var.name}" + } + ) + alarm_actions = [module.sns_topic_alarm.topic_arn] + comparison_operator = "GreaterThanOrEqualToThreshold" + evaluation_periods = 1 + threshold = 1 + period = 60 + dimensions = { + CanaryName = aws_synthetics_canary.this.name + } + namespace = "CloudWatchSynthetics" + metric_name = "Failed requests" + statistic = "Sum" +} From 648ef3efd7e1e6d15cc1b7abbd342ac56de95874 Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Mon, 5 Aug 2024 14:30:55 +0100 Subject: [PATCH 18/59] Delete redundant `canary-front-end-screenshots` canary script source code --- src/canary-front-end-screenshots/index.js | 130 ---------------------- 1 file changed, 130 deletions(-) delete mode 100644 src/canary-front-end-screenshots/index.js diff --git a/src/canary-front-end-screenshots/index.js b/src/canary-front-end-screenshots/index.js deleted file mode 100644 index e12043b9..00000000 --- a/src/canary-front-end-screenshots/index.js +++ /dev/null @@ -1,130 +0,0 @@ -const {URL} = require('url'); -const synthetics = require('Synthetics'); -const log = require('SyntheticsLogger'); -const syntheticsConfiguration = synthetics.getConfiguration(); -const syntheticsLogHelper = require('SyntheticsLogHelper'); - -function extractUrlsFromSitemap(xml) { - const urlRegex = /(.*?)<\/loc>/g; - const urls = []; - let match; - - while ((match = urlRegex.exec(xml)) !== null) { - urls.push(match[1].trim()); - } - return urls; -} - -async function parseSitemap(url) { - const response = await fetch(url); - const xmlString = await response.text(); - return extractUrlsFromSitemap(xmlString) -} - -async function fetchAndParseSitemap(url) { - try { - return await parseSitemap(url) - } catch (error) { - log.error("Error fetching or parsing XML:", error); - } -} - -const loadBlueprint = async function () { - const urls = await fetchAndParseSitemap(process.env.SITEMAP_URL) - - // Set screenshot option - const takeScreenshot = true; - - /* Disabling default step screen shots taken during Synthetics.executeStep() calls - * Step will be used to publish metrics on time taken to load dom content but - * Screenshots will be taken outside the executeStep to allow for page to completely load with domcontentloaded - * You can change it to load, networkidle0, networkidle2 depending on what works best for you. - */ - syntheticsConfiguration.disableStepScreenshots(); - syntheticsConfiguration.setConfig({ - continueOnStepFailure: true, - includeRequestHeaders: true, // Enable if headers should be displayed in HAR - includeResponseHeaders: true, // Enable if headers should be displayed in HAR - restrictedHeaders: [], // Value of these headers will be redacted from logs and reports - restrictedUrlParameters: [] // Values of these url parameters will be redacted from logs and reports - - }); - - let page = await synthetics.getPage(); - for (const url of urls) { - await loadUrl(page, url, takeScreenshot); - } -}; - -const resetPage = async function (page) { - try { - await page.goto('about:blank', {waitUntil: ['load', 'networkidle0'], timeout: 30000}); - } catch (e) { - synthetics.addExecutionError('Unable to open a blank page. ', e); - } -} - -const loadUrl = async function (page, url, takeScreenshot) { - let stepName = null; - let domcontentloaded = false; - - try { - stepName = new URL(url).hostname; - } catch (e) { - const errorString = `Error parsing url: ${url}. ${e}`; - log.error(errorString); - /* If we fail to parse the URL, don't emit a metric with a stepName based on it. - It may not be a legal CloudWatch metric dimension name and we may not have an alarms - setup on the malformed URL stepName. Instead, fail this step which will - show up in the logs and will fail the overall canary and alarm on the overall canary - success rate. - */ - throw e; - } - - await synthetics.executeStep(stepName, async function () { - const sanitizedUrl = syntheticsLogHelper.getSanitizedUrl(url); - - /* You can customize the wait condition here. For instance, using 'networkidle2' or 'networkidle0' to load page completely. - networkidle0: Navigation is successful when the page has had no network requests for half a second. This might never happen if page is constantly loading multiple resources. - networkidle2: Navigation is successful when the page has no more then 2 network requests for half a second. - domcontentloaded: It's fired as soon as the page DOM has been loaded, without waiting for resources to finish loading. If needed add explicit wait with await new Promise(r => setTimeout(r, milliseconds)) - */ - const response = await page.goto(url, {waitUntil: ['domcontentloaded'], timeout: 30000}); - log.info("response: ", JSON.stringify(response)) - - if (response) { - domcontentloaded = true; - const status = response.status(); - const statusText = response.statusText(); - - logResponseString = `Response from url: ${sanitizedUrl} Status: ${status} Status Text: ${statusText}`; - - //If the response status code is not a 2xx success code - if (response.status() < 200 || response.status() > 299) { - throw new Error(`Failed to load url: ${sanitizedUrl} ${response.status()} ${response.statusText()}`); - } - } else { - const logNoResponseString = `No response returned for url: ${sanitizedUrl}`; - log.error(logNoResponseString); - throw new Error(logNoResponseString); - } - }); - - // Wait for 3 seconds to let page load fully before taking screenshot. - if (domcontentloaded && takeScreenshot) { - await new Promise(r => setTimeout(r, 3000)); - await synthetics.takeScreenshot(stepName, 'loaded'); - } - - // Reset page - await resetPage(page); -}; - -async function handler() { - return await loadBlueprint(); -} - -module.exports = { - handler -} \ No newline at end of file From 213944c8891fdb8d9039704399b1cf1e34029b0e Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Mon, 5 Aug 2024 14:45:32 +0100 Subject: [PATCH 19/59] Add source code for Lambda function used to send notifications after canary run failure --- src/lambda-canary-notification/index.js | 416 ++++++++++++++++++ .../20-app/lambda.canary-notification.tf | 64 +++ 2 files changed, 480 insertions(+) create mode 100644 src/lambda-canary-notification/index.js create mode 100644 terraform/20-app/lambda.canary-notification.tf diff --git a/src/lambda-canary-notification/index.js b/src/lambda-canary-notification/index.js new file mode 100644 index 00000000..28702653 --- /dev/null +++ b/src/lambda-canary-notification/index.js @@ -0,0 +1,416 @@ +const { + S3Client, + GetObjectCommand, + ListObjectsV2Command, + ListObjectsV2CommandOutput, + GetObjectCommandOutput +} = require("@aws-sdk/client-s3"); +const {SecretsManagerClient, GetSecretValueCommand, GetSecretValueCommandOutput} = require("@aws-sdk/client-secrets-manager"); +const {WebClient} = require("@slack/web-api") +const axios = require('axios'); +const FormData = require('form-data'); +const path = require('path'); + +const S3_CANARY_LOGS_BUCKET_NAME = process.env.S3_CANARY_LOGS_BUCKET_NAME; + +/** + * Gets the filename associated with the s3 key / file path + * + * @param {string} filePath - The full filepath / s3 key for the files + * @returns {string} - The extracted filename + */ +function getFilename(filePath) { + return path.basename(filePath); +} + +/** + * Gets the secret for the Slack webhook URL from SecretsManager + * + * @param {SecretsManagerClient} secretsManagerClient - An instance of the SecretsManagerClient + * to use for sending the command. + * @returns {object} - The response from secrets manager + */ +async function getSecret(secretsManagerClient) { + const input = { + "SecretId": process.env.SECRETS_MANAGER_SLACK_WEBHOOK_URL_ARN + }; + const command = new GetSecretValueCommand(input); + return secretsManagerClient.send(command); +} + +/** + * Gets and parses the secret for the Slack webhook URL from SecretsManager + * + * @param {SecretsManagerClient} secretsManagerClient - An optional instance of the SecretsManagerClient + * to use for sending the command. + * @returns {object} - The JSON object representing the secret + */ +async function getSlackSecret(secretsManagerClient = new SecretsManagerClient()) { + const response = await getSecret(secretsManagerClient) + return JSON.parse(response.SecretString) +} + +/** + * Lists all the files in the s3 bucket which contain the given prefix + * + * @param {string} bucket - The name of the S3 bucket. + * @param {string} prefix - The prefix to filter objects by in the S3 bucket. + * @param {S3Client} s3Client - An optional instance of the S3Client to use for sending the command. + * @returns {ListObjectsV2CommandOutput} - The response from the underlying `ListObjectsV2Command` call. + */ +async function listFiles(bucket, prefix, s3Client = new S3Client()) { + const command = new ListObjectsV2Command({ + Bucket: bucket, Prefix: prefix + }); + return s3Client.send(command); +} + +/** + * Creates a file `Buffer` from the given `stream` + * + * @param {object} stream - The filestream to be opened. + * @returns {Promise} - The Promise object wrapping the created file buffer. + */ +const streamToBuffer = async (stream) => { + return new Promise((resolve, reject) => { + const chunks = []; + stream.on('data', (chunk) => chunks.push(chunk)); + stream.on('end', () => resolve(Buffer.concat(chunks))); + stream.on('error', reject); + }); +}; + +/** + * Downloads the given file from the s3 bucket + * + * @param {string} bucket - The name of the S3 bucket. + * @param {string} key - The key associated with the target file being downloaded from the S3 bucket. + * @param {S3Client} s3Client - An optional instance of the S3Client to use for sending the command. + * @returns {GetObjectCommandOutput} - The response from the underlying `GetObjectCommand` call. + */ +async function downloadFile(bucket, key, s3Client = new S3Client()) { + const command = new GetObjectCommand({ + Bucket: bucket, Key: key + }); + return s3Client.send(command); +} + +/** + * Downloads the given file from the s3 bucket + * + * @param {string} bucket - The name of the S3 bucket. + * @param {array} keys - The keys associated with the target files being downloaded from the S3 bucket. + * @param {S3Client} s3Client - An optional instance of the S3Client to use for sending the command. + * @returns {array} - A list of objects containing + * {key: : content: The individual download responses} + */ +async function downloadAllFiles(keys, bucket, s3Client = new S3Client()) { + const results = []; + + for (let key of keys) { + let content = await downloadFile(bucket, key, s3Client); + results.push({"key": key, "content": content}); + } + return results; +} + +/** + * Sets up the `WebClient` object used to interact with Slack. + * + * @param {string} slack_token - The token associated with the Slack bot. + * @returns {WebClient} - An instantiated `WebClient` instance used to interact with Slack. + */ +function buildSlackClient(slack_token) { + return new WebClient(slack_token); +} + +/** + * Gets a URL from the Slack API to upload the file to. + * + * @param {WebClient} slackClient - The `WebClient` instance used to interact with Slack. + * @param {string} filename - The name of the file being uploaded to Slack. + * @param {number} length - Size in bytes of the file being uploaded. + * @returns {Object} - The response from the underlying `WebClient.files.getUploadURLExternal` call. + */ +async function getFileUploadURLToSlack(slackClient, filename, length) { + return slackClient.files.getUploadURLExternal({ + token: slackClient.token, filename: filename, length: length + }); +}; + +/** + * Sends a POST request to the given url containing the given `fileBufferStream`. + * + * @param {string} url - The URL to POST the file to. + * @param {string} filename - The name of the file being uploaded. + * @param {Buffer} fileBufferStream - Size in bytes of the file being uploaded. + * @returns {Promise} - The promise from the underlying POST request call + */ +async function sendPostRequest(url, filename, fileBufferStream) { + try { + const form = new FormData(); + form.append('file', fileBufferStream, {filename}); + await axios.post(url, form, { + headers: {...form.getHeaders()} + }); + } catch (error) { + console.error('Error sending POST request:', error.message); + } +} + +/** + * Finishes an upload to Slack started with a `WebClient.files.getUploadURLExternal` call + * + * @param {WebClient} slackClient - The `WebClient` instance used to interact with Slack. + * @param {array} files - Array of file ids and their corresponding (optional) titles. + * @param {string} channelId - The ID associated with the channel which the image is being sent to. + * @param {string} threadTs - The ID of the parent thread to attach this image as a reply to. + * @returns {Object} - The response from the underlying `WebClient.files.completeUploadExternal` call. + */ +async function completeFileUploadToSlack(slackClient, files, channelId, threadTs) { + return slackClient.files.completeUploadExternal({ + token: slackClient.token, files: files, channel_id: channelId, thread_ts: threadTs, + }); +} + +/** + * Extracts the directory from the given full s3 object key + * + * @param {string} filePath - The `s3 object key / filepath to extract the directory from + * @returns {string} - The preceding file directory associated with the given full s3 object key. + */ +function getDirectoryPath(filePath) { + const parts = filePath.split('/'); + parts.pop(); + return parts.join('/'); +} + +/** + * Gets the current datetime containing the year, month, day and hour. The month, day and hours are padded to 2 digits. + * + * @returns {object} - The year, month, day, hour values returned as padded strings. + * e.g. The date of 5th Aug 2024 at 1pm is returned as: + * >>> {'2024', '08', '05', '13'} + */ +function getCurrentDate() { + const currentDate = new Date(); + const year = String(currentDate.getFullYear()); + const month = String(currentDate.getMonth() + 1).padStart(2, '0'); + const day = String(currentDate.getDate()).padStart(2, '0'); + const hour = String(currentDate.getHours()).padStart(2, '0'); + return {year, month, day, hour} +} + +/** + * Gets the target folder/prefix in the s3 bucket for the required files + * + * @param {string} target - The mid-prefix / canary name to filter against. + * @param {S3Client} s3Client - An optional instance of the S3Client to use for sending the command. + * @returns {string} - The relevant prefix for the required files in the s3 bucket + */ +async function getRelevantPrefix(target, s3Client = new S3Client()) { + const {year, month, day, hour} = getCurrentDate() + const key = `canary/eu-west-2/${target}/${year}/${month}/${day}/${hour}` + try { + const data = await listFiles(S3_CANARY_LOGS_BUCKET_NAME, key) + + const folders = new Set(data.Contents.map(item => { + const parts = item.Key.split('/'); + return parts.slice(0, 9).join('/'); + })); + + const folderArray = Array.from(folders); + + folderArray.sort((a, b) => (a < b ? 1 : -1)); + return getDirectoryPath(folderArray[0]) + } catch (error) { + console.error('Error fetching the latest folder:', error); + throw error; + } +} + +/** + * Extracts the keys associated with the failed page snapshots from the given keys + * + * @param {array} keys - Array of objects representing each of the keys in the s3 folder/prefix + * @returns {array} - Arrays of strings representing the keys of the failed page snapshots. + */ +function extractScreenshotKeys(keys) { + return keys + .filter(item => item.Key.includes('-succeeded') && item.Key.endsWith('.png')) + .map(item => item.Key); +} + +/** + * Extracts the key associated with the `SyntheticsReport` file from the given keys + * + * @param {array} keys - Array of objects representing each of the keys in the s3 folder/prefix + * @returns {string} - The key associated with the `SyntheticsReport` file + */ +function extractReportKey(keys) { + const reportKeys = keys + .filter(item => item.Key.includes('SyntheticsReport')) + .map(item => item.Key); + return reportKeys[0] +} + +/** + * Builds the `blocks` to be sent to the Slack API to post the primary message. + * + * @param {string} target - The name of the canary from which the alarm was raised. + * @param {string} startTime - The time which the canary started its run. + * @param {string} endTime - The time which the canary completed its run. + * + * @returns {array} - An array of JSON objects which can be used to post to the Slack channel with. + */ +function buildSlackPostPayload(target, startTime, endTime) { + return [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": ":alert: Canary run failed", + "emoji": true + } + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": `*Alarm name:*\n${target}` + } + }, + { + "type": "context", + "elements": [ + { + "type": "plain_text", + "text": `Canary started at ${startTime} and failed at ${endTime}` + } + ] + } + ] +} + +/** + * Uploads the downloaded file to the given Slack channel under the parent thread as an individual reply. + * + * @param {object} downloadedFileResponse - The object containing the key of file and the `GetObjectCommandResponse` + * @param {WebClient} slackClient - The `WebClient` instance used to interact with Slack. + * @param {string} channelId - The ID associated with the channel which the image is being sent to. + * @param {string} threadTs - The ID of the parent thread to attach this image as a reply to. + * + * @returns {Promise} - The promise from the function call + */ +async function uploadScreenshotToSlackThread(downloadedFileResponse, slackClient, channelId, threadTs) { + const fileBufferStream = await streamToBuffer(downloadedFileResponse.content.Body); + const fileName = getFilename(downloadedFileResponse.key); + const fileUploadURLResponse = await getFileUploadURLToSlack(slackClient, fileName, fileBuffer.length) + + const uploadURL = fileUploadURLResponse.upload_url + const fileID = fileUploadURLResponse.file_id + + await sendPostRequest(uploadURL, fileName, fileBufferStream) + const files = [{"id": fileID, "title": fileName}] + await completeFileUploadToSlack(slackClient, files, channelId, threadTs) +} + +/** + * Uploads the downloaded files to the given Slack channel under the parent thread as an individual reply. + * + * @param {array} downloadedFileResponses - The objects containing the key of each file and `GetObjectCommandResponse` + * @param {WebClient} slackClient - The `WebClient` instance used to interact with Slack. + * @param {string} channelId - The ID associated with the channel which the image is being sent to. + * @param {string} threadTs - The ID of the parent thread to attach this image as a reply to. + * + * @returns {Promise} - The promise from the function call + */ +async function uploadAllScreenshotsToSlackThread(downloadedFileResponses, slackClient, channelId, threadTs) { + for (const downloadedFileResponse of downloadedFileResponses) { + await uploadScreenshotToSlackThread(downloadedFileResponse, slackClient, channelId, threadTs) + } +} + +/** + * Posts a primary/parent message to Slack + * + * @param {WebClient} slackClient - The `WebClient` instance used to interact with Slack. + * @param {array} payload - The `blocks` payload to send with the message. + * @param {string} channelId - The ID associated with the channel which the image is being sent to. + * @returns {Object} - The response from the underlying `WebClient.chat.postMessage` call. + */ +async function sendSlackPost(slackClient, payload, channelId) { + return await slackClient.chat.postMessage({ + token: slackClient.token, channel: channelId, blocks: payload, text: 'Synthetic monitoring alert raised', + }); +} + +/** + * Extracts the contents of the `SyntheticsReport` file from the given `folderContents` + * + * @param {array} folderContents - Array of objects representing the listed folder contents + * @returns {object} - The JSON representation of the contents of the `SyntheticReports` file. + */ +async function extractReport(folderContents) { + const downloadedReportKey = extractReportKey(folderContents) + const downloadedReportResponse = await downloadFile(S3_CANARY_LOGS_BUCKET_NAME, downloadedReportKey,) + + const reportFileBuffer = await streamToBuffer(downloadedReportResponse.Body); + + const jsonString = reportFileBuffer.toString('utf8'); + return JSON.parse(jsonString); +} + +/** + * Extracts the name of the triggered Canary from the `event` object passed to the Lambda runtime. + * + * @param {object} event - The object passed down to the Lambda runtime on initialization. + * @returns {string} - The name of the Canary being triggered. + */ +function extractTargetFromEvent(event) { + const eventMessage = JSON.parse(event.Records[0].Sns.Message); + return eventMessage.Trigger.Dimensions[0].value +} + +/** + * Calculates the relevant folder in s3 relating to the triggered Canary results. + * + * @param {object} event - The object passed down to the Lambda runtime on initialization. + * @returns {string} - The prefix associated with the 'folder' of the triggered Canary results. + */ +async function determineRelevantFolderInS3(event) { + const target = extractTargetFromEvent(event) + return getRelevantPrefix(target) +} + + +/** + * Main handler entrypoint for the Lambda runtime execution. + * + * @param {object} event - The object passed down to the Lambda runtime on initialization. + */ +async function handler(event) { + const relevantFolder = determineRelevantFolderInS3(event) + const slackSecret = await getSlackSecret() + + const slackClient = await buildSlackClient(slackSecret.slack_token) + + const listedFiles = await listFiles(S3_CANARY_LOGS_BUCKET_NAME, relevantFolder) + const folderContents = listedFiles.Contents + + const report = await extractReport(folderContents) + const slackPayload = buildSlackPostPayload(report.canaryName, report.startTime, report.endTime,) + const slackPostResponse = await sendSlackPost(slackClient, slackPayload, slackSecret.slack_channel_id,) + + const extractedSnapshotKeys = extractScreenshotKeys(folderContents) + const downloadResponses = downloadAllFiles(extractedSnapshotKeys, S3_CANARY_LOGS_BUCKET_NAME) + + await uploadAllScreenshotsToSlackThread(downloadResponses, slackClient, slackSecret.slack_channel_id, slackPostResponse.ts) +} + +module.exports = { + handler +} \ No newline at end of file diff --git a/terraform/20-app/lambda.canary-notification.tf b/terraform/20-app/lambda.canary-notification.tf new file mode 100644 index 00000000..d0d9d566 --- /dev/null +++ b/terraform/20-app/lambda.canary-notification.tf @@ -0,0 +1,64 @@ +module "lambda_canary_notification" { + source = "terraform-aws-modules/lambda/aws" + version = "7.7.0" + function_name = "${local.prefix}-canary-notification" + description = "Sends notifications when a synthetics canary run fails." + + create_package = true + runtime = "nodejs18.x" + handler = "index.handler" + source_path = "../../src/lambda-canary-notification" + + timeout = 120 + + architectures = ["arm64"] + maximum_retry_attempts = 1 + + environment_variables = { + SECRETS_MANAGER_SLACK_WEBHOOK_URL_ARN = aws_secretsmanager_secret.slack_webhook_url.arn + S3_CANARY_LOGS_BUCKET_NAME = module.s3_canary_logs.s3_bucket_id + } + + attach_policy_statements = true + policy_statements = { + get_screenshots_from_s3_bucket = { + effect = "Allow", + actions = ["s3:GetObject"] + resources = ["${module.s3_canary_logs.s3_bucket_arn}/*"] + } + list_objects_in_s3_bucket = { + effect = "Allow", + actions = ["s3:ListBucket"] + resources = [module.s3_canary_logs.s3_bucket_arn] + } + get_slack_webhook_url_from_secrets_manager = { + effect = "Allow", + actions = ["secretsmanager:GetSecretValue"], + resources = [aws_secretsmanager_secret.slack_webhook_url.arn] + } + } + + create_current_version_allowed_triggers = false + allowed_triggers = { + sns_cloudfront_alarms = { + principal = "sns.amazonaws.com" + source_arn = module.cloudwatch_canary_front_end_screenshots.sns_topic_arn + } + } +} + +module "lambda_canary_notification_security_group" { + source = "terraform-aws-modules/security-group/aws" + version = "5.1.0" + + name = "${local.prefix}-lambda-canary-notification" + vpc_id = module.vpc.vpc_id + + egress_with_cidr_blocks = [ + { + description = "https to internet" + rule = "https-443-tcp" + cidr_blocks = "0.0.0.0/0" + } + ] +} From 50ae722b0b3bbcca2fa13afcf1dd38c342f46d98 Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Mon, 5 Aug 2024 14:47:13 +0100 Subject: [PATCH 20/59] Add packaging for `lambda-canary-notification` --- src/lambda-canary-notification/jest.config.js | 4 + .../package-lock.json | 5367 +++++++++++++++++ src/lambda-canary-notification/package.json | 24 + 3 files changed, 5395 insertions(+) create mode 100644 src/lambda-canary-notification/jest.config.js create mode 100644 src/lambda-canary-notification/package-lock.json create mode 100644 src/lambda-canary-notification/package.json diff --git a/src/lambda-canary-notification/jest.config.js b/src/lambda-canary-notification/jest.config.js new file mode 100644 index 00000000..ad4cfd47 --- /dev/null +++ b/src/lambda-canary-notification/jest.config.js @@ -0,0 +1,4 @@ +module.exports = { + reporters: ["default", "jest-junit"], + coverageReporters: ["json-summary", "text"], +}; diff --git a/src/lambda-canary-notification/package-lock.json b/src/lambda-canary-notification/package-lock.json new file mode 100644 index 00000000..669e1df3 --- /dev/null +++ b/src/lambda-canary-notification/package-lock.json @@ -0,0 +1,5367 @@ +{ + "name": "lambda-canary-notification", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "lambda-canary-notification", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@aws-sdk/client-s3": "^3.556.0", + "@slack/web-api": "^7.3.1", + "@slack/webhook": "^7.0.2", + "axios": "^1.7.2", + "form-data": "^4.0.0" + }, + "devDependencies": { + "jest": "^29.7.0", + "jest-junit": "^16.0.0", + "sinon": "^17.0.1" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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==", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "dependencies": { + "@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/@aws-crypto/sha1-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==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-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==", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-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==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.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==", + "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/@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==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "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==", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "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==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "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==", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "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==", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "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==", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "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==", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "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==", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.623.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.623.0.tgz", + "integrity": "sha512-vEroSYEtbp5n289xsQnnAhKxg3R5NGkbhKXWpW1m7GGDsFihwVT9CVsDHpIW2Hvezz5ob65gB4ZAYMnJWZuUpA==", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.623.0", + "@aws-sdk/client-sts": "3.623.0", + "@aws-sdk/core": "3.623.0", + "@aws-sdk/credential-provider-node": "3.623.0", + "@aws-sdk/middleware-bucket-endpoint": "3.620.0", + "@aws-sdk/middleware-expect-continue": "3.620.0", + "@aws-sdk/middleware-flexible-checksums": "3.620.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-location-constraint": "3.609.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-sdk-s3": "3.622.0", + "@aws-sdk/middleware-signing": "3.620.0", + "@aws-sdk/middleware-ssec": "3.609.0", + "@aws-sdk/middleware-user-agent": "3.620.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/signature-v4-multi-region": "3.622.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@aws-sdk/xml-builder": "3.609.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.2", + "@smithy/eventstream-serde-browser": "^3.0.5", + "@smithy/eventstream-serde-config-resolver": "^3.0.3", + "@smithy/eventstream-serde-node": "^3.0.4", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-blob-browser": "^3.1.2", + "@smithy/hash-node": "^3.0.3", + "@smithy/hash-stream-node": "^3.1.2", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/md5-js": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.14", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-stream": "^3.1.3", + "@smithy/util-utf8": "^3.0.0", + "@smithy/util-waiter": "^3.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.623.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.623.0.tgz", + "integrity": "sha512-oEACriysQMnHIVcNp7TD6D1nzgiHfYK0tmMBMbUxgoFuCBkW9g9QYvspHN+S9KgoePfMEXHuPUe9mtG9AH9XeA==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.623.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.620.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.2", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.14", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.623.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.623.0.tgz", + "integrity": "sha512-lMFEXCa6ES/FGV7hpyrppT1PiAkqQb51AbG0zVU3TIgI2IO4XX02uzMUXImRSRqRpGymRCbJCaCs9LtKvS/37Q==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.623.0", + "@aws-sdk/credential-provider-node": "3.623.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.620.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.2", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.14", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.623.0" + } + }, + "node_modules/@aws-sdk/client-sts": { + "version": "3.623.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.623.0.tgz", + "integrity": "sha512-iJNdx76SOw0YjHAUv8aj3HXzSu3TKI7qSGuR+OGATwA/kpJZDd+4+WYBdGtr8YK+hPrGGqhfecuCkEg805O5iA==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.623.0", + "@aws-sdk/core": "3.623.0", + "@aws-sdk/credential-provider-node": "3.623.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.620.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.2", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.14", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.623.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.623.0.tgz", + "integrity": "sha512-8Toq3X6trX/67obSdh4K0MFQY4f132bEbr1i0YPDWk/O3KdBt12mLC/sW3aVRnlIs110XMuX9yrWWqJ8fDW10g==", + "dependencies": { + "@smithy/core": "^2.3.2", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/signature-v4": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/util-middleware": "^3.0.3", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.620.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.620.1.tgz", + "integrity": "sha512-ExuILJ2qLW5ZO+rgkNRj0xiAipKT16Rk77buvPP8csR7kkCflT/gXTyzRe/uzIiETTxM7tr8xuO9MP/DQXqkfg==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.622.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.622.0.tgz", + "integrity": "sha512-VUHbr24Oll1RK3WR8XLUugLpgK9ZuxEm/NVeVqyFts1Ck9gsKpRg1x4eH7L7tW3SJ4TDEQNMbD7/7J+eoL2svg==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/property-provider": "^3.1.3", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/util-stream": "^3.1.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.623.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.623.0.tgz", + "integrity": "sha512-kvXA1SwGneqGzFwRZNpESitnmaENHGFFuuTvgGwtMe7mzXWuA/LkXdbiHmdyAzOo0iByKTCD8uetuwh3CXy4Pw==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.620.1", + "@aws-sdk/credential-provider-http": "3.622.0", + "@aws-sdk/credential-provider-process": "3.620.1", + "@aws-sdk/credential-provider-sso": "3.623.0", + "@aws-sdk/credential-provider-web-identity": "3.621.0", + "@aws-sdk/types": "3.609.0", + "@smithy/credential-provider-imds": "^3.2.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.623.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.623.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.623.0.tgz", + "integrity": "sha512-qDwCOkhbu5PfaQHyuQ+h57HEx3+eFhKdtIw7aISziWkGdFrMe07yIBd7TJqGe4nxXnRF1pfkg05xeOlMId997g==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.620.1", + "@aws-sdk/credential-provider-http": "3.622.0", + "@aws-sdk/credential-provider-ini": "3.623.0", + "@aws-sdk/credential-provider-process": "3.620.1", + "@aws-sdk/credential-provider-sso": "3.623.0", + "@aws-sdk/credential-provider-web-identity": "3.621.0", + "@aws-sdk/types": "3.609.0", + "@smithy/credential-provider-imds": "^3.2.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.620.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.620.1.tgz", + "integrity": "sha512-hWqFMidqLAkaV9G460+1at6qa9vySbjQKKc04p59OT7lZ5cO5VH5S4aI05e+m4j364MBROjjk2ugNvfNf/8ILg==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.623.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.623.0.tgz", + "integrity": "sha512-70LZhUb3l7cttEsg4A0S4Jq3qrCT/v5Jfyl8F7w1YZJt5zr3oPPcvDJxo/UYckFz4G4/5BhGa99jK8wMlNE9QA==", + "dependencies": { + "@aws-sdk/client-sso": "3.623.0", + "@aws-sdk/token-providers": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.621.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.621.0.tgz", + "integrity": "sha512-w7ASSyfNvcx7+bYGep3VBgC3K6vEdLmlpjT7nSIHxxQf+WSdvy+HynwJosrpZax0sK5q0D1Jpn/5q+r5lwwW6w==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.621.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.620.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.620.0.tgz", + "integrity": "sha512-eGLL0W6L3HDb3OACyetZYOWpHJ+gLo0TehQKeQyy2G8vTYXqNTeqYhuI6up9HVjBzU9eQiULVQETmgQs7TFaRg==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-arn-parser": "3.568.0", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", + "@smithy/util-config-provider": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.620.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.620.0.tgz", + "integrity": "sha512-QXeRFMLfyQ31nAHLbiTLtk0oHzG9QLMaof5jIfqcUwnOkO8YnQdeqzakrg1Alpy/VQ7aqzIi8qypkBe2KXZz0A==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.620.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.620.0.tgz", + "integrity": "sha512-ftz+NW7qka2sVuwnnO1IzBku5ccP+s5qZGeRTPgrKB7OzRW85gthvIo1vQR2w+OwHFk7WJbbhhWwbCbktnP4UA==", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-sdk/types": "3.609.0", + "@smithy/is-array-buffer": "^3.0.0", + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.620.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.620.0.tgz", + "integrity": "sha512-VMtPEZwqYrII/oUkffYsNWY9PZ9xpNJpMgmyU0rlDQ25O1c0Hk3fJmZRe6pEkAJ0omD7kLrqGl1DUjQVxpd/Rg==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.609.0.tgz", + "integrity": "sha512-xzsdoTkszGVqGVPjUmgoP7TORiByLueMHieI1fhQL888WPdqctwAx3ES6d/bA9Q/i8jnc6hs+Fjhy8UvBTkE9A==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.609.0.tgz", + "integrity": "sha512-S62U2dy4jMDhDFDK5gZ4VxFdWzCtLzwbYyFZx2uvPYTECkepLUfzLic2BHg2Qvtu4QjX+oGE3P/7fwaGIsGNuQ==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.620.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.620.0.tgz", + "integrity": "sha512-nh91S7aGK3e/o1ck64sA/CyoFw+gAYj2BDOnoNa6ouyCrVJED96ZXWbhye/fz9SgmNUZR2g7GdVpiLpMKZoI5w==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.622.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.622.0.tgz", + "integrity": "sha512-tX9wZ2ALx5Ez4bkY+SvSj6DpNZ6TmY4zlsVsdgV95LZFLjNwqnZkKkS+uKnsIyLBiBp6g92JVQwnUEIp7ov2Zw==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-arn-parser": "3.568.0", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/signature-v4": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-stream": "^3.1.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-signing": { + "version": "3.620.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.620.0.tgz", + "integrity": "sha512-gxI7rubiaanUXaLfJ4NybERa9MGPNg2Ycl/OqANsozrBnR3Pw8vqy3EuVImQOyn2pJ2IFvl8ZPoSMHf4pX56FQ==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/protocol-http": "^4.1.0", + "@smithy/signature-v4": "^4.1.0", + "@smithy/types": "^3.3.0", + "@smithy/util-middleware": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.609.0.tgz", + "integrity": "sha512-GZSD1s7+JswWOTamVap79QiDaIV7byJFssBW68GYjyRS5EBjNfwA/8s+6uE6g39R3ojyTbYOmvcANoZEhSULXg==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.620.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.620.0.tgz", + "integrity": "sha512-bvS6etn+KsuL32ubY5D3xNof1qkenpbJXf/ugGXbg0n98DvDFQ/F+SMLxHgbnER5dsKYchNnhmtI6/FC3HFu/A==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.614.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.614.0.tgz", + "integrity": "sha512-vDCeMXvic/LU0KFIUjpC3RiSTIkkvESsEfbVHiHH0YINfl8HnEqR5rj+L8+phsCeVg2+LmYwYxd5NRz4PHxt5g==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/types": "^3.3.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.622.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.622.0.tgz", + "integrity": "sha512-K7ddofVNzwTFRjmLZLfs/v+hiE9m5LguajHk8WULxXQgkcDI3nPgOfmMMGuslYohaQhRwW+ic+dzYlateLUudQ==", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.622.0", + "@aws-sdk/types": "3.609.0", + "@smithy/protocol-http": "^4.1.0", + "@smithy/signature-v4": "^4.1.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.614.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.614.0.tgz", + "integrity": "sha512-okItqyY6L9IHdxqs+Z116y5/nda7rHxLvROxtAJdLavWTYDydxrZstImNgGWTeVdmc0xX2gJCI77UYUTQWnhRw==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.614.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.609.0.tgz", + "integrity": "sha512-+Tqnh9w0h2LcrUsdXyT1F8mNhXz+tVYBtP19LpeEGntmvHwa2XzvLUCWpoIAIVsHp5+HdB2X9Sn0KAtmbFXc2Q==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.568.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.568.0.tgz", + "integrity": "sha512-XUKJWWo+KOB7fbnPP0+g/o5Ulku/X53t7i/h+sPHr5xxYTJJ9CYnbToo95mzxe7xWvkLrsNtJ8L+MnNn9INs2w==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.614.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.614.0.tgz", + "integrity": "sha512-wK2cdrXHH4oz4IomV/yrGkftU9A+ITB6nFL+rxxyO78is2ifHJpFdV4aqk4LSkXYPi6CXWNru/Dqc7yiKXgJPw==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/types": "^3.3.0", + "@smithy/util-endpoints": "^2.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.568.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.568.0.tgz", + "integrity": "sha512-3nh4TINkXYr+H41QaPelCceEB2FXP3fxp93YZXB/kqJvX0U9j0N0Uk45gvsjmEPzG8XxkPEeLIfT2I1M7A6Lig==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.609.0.tgz", + "integrity": "sha512-fojPU+mNahzQ0YHYBsx0ZIhmMA96H+ZIZ665ObU9tl+SGdbLneVZVikGve+NmHTQwHzwkFsZYYnVKAkreJLAtA==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/types": "^3.3.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.614.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.614.0.tgz", + "integrity": "sha512-15ElZT88peoHnq5TEoEtZwoXTXRxNrk60TZNdpl/TUBJ5oNJ9Dqb5Z4ryb8ofN6nm9aFf59GVAerFDz8iUoHBA==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.609.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.609.0.tgz", + "integrity": "sha512-l9XxNcA4HX98rwCC2/KoiWcmEiRfZe4G+mYwDbCFT87JIMj6GBhLDkAzr/W8KAaA2IDr8Vc6J8fZPgVulxxfMA==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz", + "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", + "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/generator": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", + "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", + "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.25.2", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", + "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", + "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", + "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.2" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "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.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", + "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", + "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.25.0", + "@babel/types": "^7.25.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz", + "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.2", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", + "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "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/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "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/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "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": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "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": "^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/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "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": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "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", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "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": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, + "node_modules/@slack/logger": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@slack/logger/-/logger-4.0.0.tgz", + "integrity": "sha512-Wz7QYfPAlG/DR+DfABddUZeNgoeY7d1J39OCR2jR+v7VBsB8ezulDK5szTnDDPDwLH5IWhLvXIHlCFZV7MSKgA==", + "dependencies": { + "@types/node": ">=18.0.0" + }, + "engines": { + "node": ">= 18", + "npm": ">= 8.6.0" + } + }, + "node_modules/@slack/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@slack/types/-/types-2.12.0.tgz", + "integrity": "sha512-yFewzUomYZ2BYaGJidPuIgjoYj5wqPDmi7DLSaGIkf+rCi4YZ2Z3DaiYIbz7qb/PL2NmamWjCvB7e9ArI5HkKg==", + "engines": { + "node": ">= 12.13.0", + "npm": ">= 6.12.0" + } + }, + "node_modules/@slack/web-api": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-7.3.2.tgz", + "integrity": "sha512-hyxwSsUhpOEVN5ORB3ne7TAXQwXl0wxBAyNTCfp4Qc/o9rpEEvY4kfsx7mZF5AhYPcdEeI1XP2bSFrh3W/HGKQ==", + "dependencies": { + "@slack/logger": "^4.0.0", + "@slack/types": "^2.9.0", + "@types/node": ">=18.0.0", + "@types/retry": "0.12.0", + "axios": "^1.6.5", + "eventemitter3": "^5.0.1", + "form-data": "^4.0.0", + "is-electron": "2.2.2", + "is-stream": "^2", + "p-queue": "^6", + "p-retry": "^4", + "retry": "^0.13.1" + }, + "engines": { + "node": ">= 18", + "npm": ">= 8.6.0" + } + }, + "node_modules/@slack/webhook": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@slack/webhook/-/webhook-7.0.2.tgz", + "integrity": "sha512-dsrO/ow6a6+xkLm/lZKbUNTsFJlBc679tD+qwlVTztsQkDxPLH6odM7FKALz1IHa+KpLX8HKUIPV13a7y7z29w==", + "dependencies": { + "@slack/types": "^2.9.0", + "@types/node": ">=18.0.0", + "axios": "^1.6.3" + }, + "engines": { + "node": ">= 18", + "npm": ">= 8.6.0" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.1.tgz", + "integrity": "sha512-MBJBiidoe+0cTFhyxT8g+9g7CeVccLM0IOKKUMCNQ1CNMJ/eIfoo0RTfVrXOONEI1UCN1W+zkiHSbzUNE9dZtQ==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-3.0.0.tgz", + "integrity": "sha512-sbnURCwjF0gSToGlsBiAmd1lRCmSn72nu9axfJu5lIx6RUEgHu6GwTMbqCdhQSi0Pumcm5vFxsi9XWXb2mTaoA==", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-3.0.0.tgz", + "integrity": "sha512-VDkpCYW+peSuM4zJip5WDfqvg2Mo/e8yxOv3VF1m11y7B8KKMKVFtmZWDe36Fvk8rGuWrPZHHXZ7rR7uM5yWyg==", + "dependencies": { + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-3.0.5.tgz", + "integrity": "sha512-SkW5LxfkSI1bUC74OtfBbdz+grQXYiPYolyu8VfpLIjEoN/sHVBlLeGXMQ1vX4ejkgfv6sxVbQJ32yF2cl1veA==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.4", + "@smithy/types": "^3.3.0", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-2.3.2.tgz", + "integrity": "sha512-in5wwt6chDBcUv1Lw1+QzZxN9fBffi+qOixfb65yK4sDuKG7zAUO9HAFqmVzsZM3N+3tTyvZjtnDXePpvp007Q==", + "dependencies": { + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.14", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/util-middleware": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-3.2.0.tgz", + "integrity": "sha512-0SCIzgd8LYZ9EJxUjLXBmEKSZR/P/w6l7Rz/pab9culE/RWuqelAKGJvn5qUOl8BgX8Yj5HWM50A5hiB/RzsgA==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.4", + "@smithy/property-provider": "^3.1.3", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-3.1.2.tgz", + "integrity": "sha512-0mBcu49JWt4MXhrhRAlxASNy0IjDRFU+aWNDRal9OtUJvJNiwDuyKMUONSOjLjSCeGwZaE0wOErdqULer8r7yw==", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^3.3.0", + "@smithy/util-hex-encoding": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-3.0.5.tgz", + "integrity": "sha512-dEyiUYL/ekDfk+2Ra4GxV+xNnFoCmk1nuIXg+fMChFTrM2uI/1r9AdiTYzPqgb72yIv/NtAj6C3dG//1wwgakQ==", + "dependencies": { + "@smithy/eventstream-serde-universal": "^3.0.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-3.0.3.tgz", + "integrity": "sha512-NVTYjOuYpGfrN/VbRQgn31x73KDLfCXCsFdad8DiIc3IcdxL+dYA9zEQPyOP7Fy2QL8CPy2WE4WCUD+ZsLNfaQ==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-3.0.4.tgz", + "integrity": "sha512-mjlG0OzGAYuUpdUpflfb9zyLrBGgmQmrobNT8b42ZTsGv/J03+t24uhhtVEKG/b2jFtPIHF74Bq+VUtbzEKOKg==", + "dependencies": { + "@smithy/eventstream-serde-universal": "^3.0.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-3.0.4.tgz", + "integrity": "sha512-Od9dv8zh3PgOD7Vj4T3HSuox16n0VG8jJIM2gvKASL6aCtcS8CfHZDWe1Ik3ZXW6xBouU+45Q5wgoliWDZiJ0A==", + "dependencies": { + "@smithy/eventstream-codec": "^3.1.2", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-3.2.4.tgz", + "integrity": "sha512-kBprh5Gs5h7ug4nBWZi1FZthdqSM+T7zMmsZxx0IBvWUn7dK3diz2SHn7Bs4dQGFDk8plDv375gzenDoNwrXjg==", + "dependencies": { + "@smithy/protocol-http": "^4.1.0", + "@smithy/querystring-builder": "^3.0.3", + "@smithy/types": "^3.3.0", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/hash-blob-browser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-3.1.2.tgz", + "integrity": "sha512-hAbfqN2UbISltakCC2TP0kx4LqXBttEv2MqSPE98gVuDFMf05lU+TpC41QtqGP3Ff5A3GwZMPfKnEy0VmEUpmg==", + "dependencies": { + "@smithy/chunked-blob-reader": "^3.0.0", + "@smithy/chunked-blob-reader-native": "^3.0.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/hash-node": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-3.0.3.tgz", + "integrity": "sha512-2ctBXpPMG+B3BtWSGNnKELJ7SH9e4TNefJS0cd2eSkOOROeBnnVBnAy9LtJ8tY4vUEoe55N4CNPxzbWvR39iBw==", + "dependencies": { + "@smithy/types": "^3.3.0", + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/hash-stream-node": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-3.1.2.tgz", + "integrity": "sha512-PBgDMeEdDzi6JxKwbfBtwQG9eT9cVwsf0dZzLXoJF4sHKHs5HEo/3lJWpn6jibfJwT34I1EBXpBnZE8AxAft6g==", + "dependencies": { + "@smithy/types": "^3.3.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-3.0.3.tgz", + "integrity": "sha512-ID1eL/zpDULmHJbflb864k72/SNOZCADRc9i7Exq3RUNJw6raWUSlFEQ+3PX3EYs++bTxZB2dE9mEHTQLv61tw==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-3.0.0.tgz", + "integrity": "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-3.0.3.tgz", + "integrity": "sha512-O/SAkGVwpWmelpj/8yDtsaVe6sINHLB1q8YE/+ZQbDxIw3SRLbTZuRaI10K12sVoENdnHqzPp5i3/H+BcZ3m3Q==", + "dependencies": { + "@smithy/types": "^3.3.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-3.0.5.tgz", + "integrity": "sha512-ILEzC2eyxx6ncej3zZSwMpB5RJ0zuqH7eMptxC4KN3f+v9bqT8ohssKbhNR78k/2tWW+KS5Spw+tbPF4Ejyqvw==", + "dependencies": { + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-3.1.0.tgz", + "integrity": "sha512-5y5aiKCEwg9TDPB4yFE7H6tYvGFf1OJHNczeY10/EFF8Ir8jZbNntQJxMWNfeQjC1mxPsaQ6mR9cvQbf+0YeMw==", + "dependencies": { + "@smithy/middleware-serde": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-middleware": "^3.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "3.0.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.14.tgz", + "integrity": "sha512-7ZaWZJOjUxa5hgmuMspyt8v/zVsh0GXYuF7OvCmdcbVa/xbnKQoYC+uYKunAqRGTkxjOyuOCw9rmFUFOqqC0eQ==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/service-error-classification": "^3.0.3", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-3.0.3.tgz", + "integrity": "sha512-puUbyJQBcg9eSErFXjKNiGILJGtiqmuuNKEYNYfUD57fUl4i9+mfmThtQhvFXU0hCVG0iEJhvQUipUf+/SsFdA==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-3.0.3.tgz", + "integrity": "sha512-r4klY9nFudB0r9UdSMaGSyjyQK5adUyPnQN/ZM6M75phTxOdnc/AhpvGD1fQUvgmqjQEBGCwpnPbDm8pH5PapA==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.4.tgz", + "integrity": "sha512-YvnElQy8HR4vDcAjoy7Xkx9YT8xZP4cBXcbJSgm/kxmiQu08DwUwj8rkGnyoJTpfl/3xYHH+d8zE+eHqoDCSdQ==", + "dependencies": { + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.1.4.tgz", + "integrity": "sha512-+UmxgixgOr/yLsUxcEKGH0fMNVteJFGkmRltYFHnBMlogyFdpzn2CwqWmxOrfJELhV34v0WSlaqG1UtE1uXlJg==", + "dependencies": { + "@smithy/abort-controller": "^3.1.1", + "@smithy/protocol-http": "^4.1.0", + "@smithy/querystring-builder": "^3.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-3.1.3.tgz", + "integrity": "sha512-zahyOVR9Q4PEoguJ/NrFP4O7SMAfYO1HLhB18M+q+Z4KFd4V2obiMnlVoUFzFLSPeVt1POyNWneHHrZaTMoc/g==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.1.0.tgz", + "integrity": "sha512-dPVoHYQ2wcHooGXg3LQisa1hH0e4y0pAddPMeeUPipI1tEOqL6A4N0/G7abeq+K8wrwSgjk4C0wnD1XZpJm5aA==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.3.tgz", + "integrity": "sha512-vyWckeUeesFKzCDaRwWLUA1Xym9McaA6XpFfAK5qI9DKJ4M33ooQGqvM4J+LalH4u/Dq9nFiC8U6Qn1qi0+9zw==", + "dependencies": { + "@smithy/types": "^3.3.0", + "@smithy/util-uri-escape": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-3.0.3.tgz", + "integrity": "sha512-zahM1lQv2YjmznnfQsWbYojFe55l0SLG/988brlLv1i8z3dubloLF+75ATRsqPBboUXsW6I9CPGE5rQgLfY0vQ==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-3.0.3.tgz", + "integrity": "sha512-Jn39sSl8cim/VlkLsUhRFq/dKDnRUFlfRkvhOJaUbLBXUsLRLNf9WaxDv/z9BjuQ3A6k/qE8af1lsqcwm7+DaQ==", + "dependencies": { + "@smithy/types": "^3.3.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.4.tgz", + "integrity": "sha512-qMxS4hBGB8FY2GQqshcRUy1K6k8aBWP5vwm8qKkCT3A9K2dawUwOIJfqh9Yste/Bl0J2lzosVyrXDj68kLcHXQ==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-4.1.0.tgz", + "integrity": "sha512-aRryp2XNZeRcOtuJoxjydO6QTaVhxx/vjaR+gx7ZjaFgrgPRyZ3HCTbfwqYj6ZWEBHkCSUfcaymKPURaByukag==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", + "@smithy/util-hex-encoding": "^3.0.0", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-uri-escape": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-3.1.12.tgz", + "integrity": "sha512-wtm8JtsycthkHy1YA4zjIh2thJgIQ9vGkoR639DBx5lLlLNU0v4GARpQZkr2WjXue74nZ7MiTSWfVrLkyD8RkA==", + "dependencies": { + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", + "@smithy/util-stream": "^3.1.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.3.0.tgz", + "integrity": "sha512-IxvBBCTFDHbVoK7zIxqA1ZOdc4QfM5HM7rGleCuHi7L1wnKv5Pn69xXJQ9hgxH60ZVygH9/JG0jRgtUncE3QUA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-3.0.3.tgz", + "integrity": "sha512-pw3VtZtX2rg+s6HMs6/+u9+hu6oY6U7IohGhVNnjbgKy86wcIsSZwgHrFR+t67Uyxvp4Xz3p3kGXXIpTNisq8A==", + "dependencies": { + "@smithy/querystring-parser": "^3.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/util-base64": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-3.0.0.tgz", + "integrity": "sha512-Kxvoh5Qtt0CDsfajiZOCpJxgtPHXOKwmM+Zy4waD43UoEMA+qPxxa98aE/7ZhdnBFZFXMOiBR5xbcaMhLtznQQ==", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-3.0.0.tgz", + "integrity": "sha512-cbjJs2A1mLYmqmyVl80uoLTJhAcfzMOyPgjwAYusWKMdLeNtzmMz9YxNl3/jRLoxSS3wkqkf0jwNdtXWtyEBaQ==", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-3.0.0.tgz", + "integrity": "sha512-Tj7pZ4bUloNUP6PzwhN7K386tmSmEET9QtQg0TgdNOnxhZvCssHji+oZTUIuzxECRfG8rdm2PMw2WCFs6eIYkA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-3.0.0.tgz", + "integrity": "sha512-pbjk4s0fwq3Di/ANL+rCvJMKM5bzAQdE5S/6RL5NXgMExFAi6UgQMPOm5yPaIWPpr+EOXKXRonJ3FoxKf4mCJQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "3.0.14", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.14.tgz", + "integrity": "sha512-0iwTgKKmAIf+vFLV8fji21Jb2px11ktKVxbX6LIDPAUJyWQqGqBVfwba7xwa1f2FZUoolYQgLvxQEpJycXuQ5w==", + "dependencies": { + "@smithy/property-provider": "^3.1.3", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "3.0.14", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.14.tgz", + "integrity": "sha512-e9uQarJKfXApkTMMruIdxHprhcXivH1flYCe8JRDTzkkLx8dA3V5J8GZlST9yfDiRWkJpZJlUXGN9Rc9Ade3OQ==", + "dependencies": { + "@smithy/config-resolver": "^3.0.5", + "@smithy/credential-provider-imds": "^3.2.0", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/property-provider": "^3.1.3", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-2.0.5.tgz", + "integrity": "sha512-ReQP0BWihIE68OAblC/WQmDD40Gx+QY1Ez8mTdFMXpmjfxSyz2fVQu3A4zXRfQU9sZXtewk3GmhfOHswvX+eNg==", + "dependencies": { + "@smithy/node-config-provider": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-3.0.0.tgz", + "integrity": "sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-3.0.3.tgz", + "integrity": "sha512-l+StyYYK/eO3DlVPbU+4Bi06Jjal+PFLSMmlWM1BEwyLxZ3aKkf1ROnoIakfaA7mC6uw3ny7JBkau4Yc+5zfWw==", + "dependencies": { + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-3.0.3.tgz", + "integrity": "sha512-AFw+hjpbtVApzpNDhbjNG5NA3kyoMs7vx0gsgmlJF4s+yz1Zlepde7J58zpIRIsdjc+emhpAITxA88qLkPF26w==", + "dependencies": { + "@smithy/service-error-classification": "^3.0.3", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-3.1.3.tgz", + "integrity": "sha512-FIv/bRhIlAxC0U7xM1BCnF2aDRPq0UaelqBHkM2lsCp26mcBbgI0tCVTv+jGdsQLUmAMybua/bjDsSu8RQHbmw==", + "dependencies": { + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/types": "^3.3.0", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-hex-encoding": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-3.0.0.tgz", + "integrity": "sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", + "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-3.1.2.tgz", + "integrity": "sha512-4pP0EV3iTsexDx+8PPGAKCQpd/6hsQBaQhqWzU4hqKPHN5epPsxKbvUTIiYIHTxaKt6/kEaqPBpu/ufvfbrRzw==", + "dependencies": { + "@smithy/abort-controller": "^3.1.1", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/node": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz", + "integrity": "sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==", + "dependencies": { + "undici-types": "~6.13.0" + } + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.3.tgz", + "integrity": "sha512-Ar7ND9pU99eJ9GpoGQKhKf58GpUOgnzuaB7ueNQ5BMi0p+LZ5oaEnfF999fAArcTIBwXTCHAmGcHOZJaWPq9Nw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/bowser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001649", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001649.tgz", + "integrity": "sha512-fJegqZZ0ZX8HOWr6rcafGr72+xcgJKI9oWfDW5DrD7ExUtgZC7a7R7ZYmZqplh7XDocFdGeIFn7roAxhOeYrPQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", + "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", + "dev": true + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.4.tgz", + "integrity": "sha512-orzA81VqLyIGUEA77YkVA1D+N+nNfl2isJVjjmOyrlxuooZ19ynb+dOlaDTqd/idKRS9lDCSBmtzM+kyCsMnkA==", + "dev": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-xml-parser": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", + "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-core-module": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", + "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-electron": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.2.2.tgz", + "integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "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-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "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-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-junit": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-16.0.0.tgz", + "integrity": "sha512-A94mmw6NfJab4Fg/BlvVOUXzXgF0XIH6EmTgJ5NDPp4xoKq0Kr7sErb+4Xs9nZvu58pJojz5RFGpqnZYJTrRfQ==", + "dev": true, + "dependencies": { + "mkdirp": "^1.0.4", + "strip-ansi": "^6.0.1", + "uuid": "^8.3.2", + "xml": "^1.0.1" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/jest-junit/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==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "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, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/nise": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz", + "integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" + } + }, + "node_modules/nise/node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-to-regexp": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", + "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/sinon": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.5", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz", + "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "dev": true + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/src/lambda-canary-notification/package.json b/src/lambda-canary-notification/package.json new file mode 100644 index 00000000..5bd5dab4 --- /dev/null +++ b/src/lambda-canary-notification/package.json @@ -0,0 +1,24 @@ +{ + "name": "lambda-canary-notification", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "jest --watch", + "test:ci": "jest --ci --coverage" + }, + "author": "UKHSA Data Dashboard", + "license": "ISC", + "devDependencies": { + "jest": "^29.7.0", + "jest-junit": "^16.0.0", + "sinon": "^17.0.1" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.556.0", + "@slack/webhook": "^7.0.2", + "@slack/web-api": "^7.3.1", + "axios": "^1.7.2", + "form-data": "^4.0.0" + } +} From 4880db5784eabd8353f5ca01f70249e06e5b652e Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Mon, 5 Aug 2024 14:49:50 +0100 Subject: [PATCH 21/59] Remove successful canary run reports after 1 day --- .../modules/cloud-watch-canary/cloudwatch.synthetics-canary.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terraform/modules/cloud-watch-canary/cloudwatch.synthetics-canary.tf b/terraform/modules/cloud-watch-canary/cloudwatch.synthetics-canary.tf index 8e690b9c..c4ca36d6 100644 --- a/terraform/modules/cloud-watch-canary/cloudwatch.synthetics-canary.tf +++ b/terraform/modules/cloud-watch-canary/cloudwatch.synthetics-canary.tf @@ -22,6 +22,6 @@ resource "aws_synthetics_canary" "this" { environment_variables = var.environment_variables } - success_retention_period = 7 + success_retention_period = 1 failure_retention_period = 31 } From 1aaa6071aa06b6bbcb527d4715dc0802cc4790fc Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Mon, 5 Aug 2024 14:50:09 +0100 Subject: [PATCH 22/59] Keep failed canary reports for 2 weeks --- .../modules/cloud-watch-canary/cloudwatch.synthetics-canary.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terraform/modules/cloud-watch-canary/cloudwatch.synthetics-canary.tf b/terraform/modules/cloud-watch-canary/cloudwatch.synthetics-canary.tf index c4ca36d6..e6a51382 100644 --- a/terraform/modules/cloud-watch-canary/cloudwatch.synthetics-canary.tf +++ b/terraform/modules/cloud-watch-canary/cloudwatch.synthetics-canary.tf @@ -23,5 +23,5 @@ resource "aws_synthetics_canary" "this" { } success_retention_period = 1 - failure_retention_period = 31 + failure_retention_period = 14 } From 36c70a2d4c9e6bf1c6414b5e1c783a061a93625e Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Mon, 5 Aug 2024 15:47:35 +0100 Subject: [PATCH 23/59] Fix issues around downloading snapshots --- src/lambda-canary-notification/index.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/lambda-canary-notification/index.js b/src/lambda-canary-notification/index.js index 28702653..8503d868 100644 --- a/src/lambda-canary-notification/index.js +++ b/src/lambda-canary-notification/index.js @@ -210,9 +210,9 @@ function getCurrentDate() { */ async function getRelevantPrefix(target, s3Client = new S3Client()) { const {year, month, day, hour} = getCurrentDate() - const key = `canary/eu-west-2/${target}/${year}/${month}/${day}/${hour}` + const prefix = `canary/eu-west-2/${target}/${year}/${month}/${day}/${hour}` try { - const data = await listFiles(S3_CANARY_LOGS_BUCKET_NAME, key) + const data = await listFiles(S3_CANARY_LOGS_BUCKET_NAME, prefix, s3Client) const folders = new Set(data.Contents.map(item => { const parts = item.Key.split('/'); @@ -308,7 +308,7 @@ function buildSlackPostPayload(target, startTime, endTime) { async function uploadScreenshotToSlackThread(downloadedFileResponse, slackClient, channelId, threadTs) { const fileBufferStream = await streamToBuffer(downloadedFileResponse.content.Body); const fileName = getFilename(downloadedFileResponse.key); - const fileUploadURLResponse = await getFileUploadURLToSlack(slackClient, fileName, fileBuffer.length) + const fileUploadURLResponse = await getFileUploadURLToSlack(slackClient, fileName, fileBufferStream.length) const uploadURL = fileUploadURLResponse.upload_url const fileID = fileUploadURLResponse.file_id @@ -329,7 +329,7 @@ async function uploadScreenshotToSlackThread(downloadedFileResponse, slackClient * @returns {Promise} - The promise from the function call */ async function uploadAllScreenshotsToSlackThread(downloadedFileResponses, slackClient, channelId, threadTs) { - for (const downloadedFileResponse of downloadedFileResponses) { + for (let downloadedFileResponse of downloadedFileResponses) { await uploadScreenshotToSlackThread(downloadedFileResponse, slackClient, channelId, threadTs) } } @@ -383,17 +383,16 @@ function extractTargetFromEvent(event) { */ async function determineRelevantFolderInS3(event) { const target = extractTargetFromEvent(event) - return getRelevantPrefix(target) + return await getRelevantPrefix(target) } - /** * Main handler entrypoint for the Lambda runtime execution. * * @param {object} event - The object passed down to the Lambda runtime on initialization. */ async function handler(event) { - const relevantFolder = determineRelevantFolderInS3(event) + const relevantFolder = await determineRelevantFolderInS3(event) const slackSecret = await getSlackSecret() const slackClient = await buildSlackClient(slackSecret.slack_token) @@ -406,8 +405,8 @@ async function handler(event) { const slackPostResponse = await sendSlackPost(slackClient, slackPayload, slackSecret.slack_channel_id,) const extractedSnapshotKeys = extractScreenshotKeys(folderContents) - const downloadResponses = downloadAllFiles(extractedSnapshotKeys, S3_CANARY_LOGS_BUCKET_NAME) + const downloadResponses = await downloadAllFiles(extractedSnapshotKeys, S3_CANARY_LOGS_BUCKET_NAME) await uploadAllScreenshotsToSlackThread(downloadResponses, slackClient, slackSecret.slack_channel_id, slackPostResponse.ts) } From 5809baa45af025b3b0f36061447a697fb8cf7ceb Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Mon, 5 Aug 2024 15:48:22 +0100 Subject: [PATCH 24/59] Rename function --- src/lambda-canary-notification/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lambda-canary-notification/index.js b/src/lambda-canary-notification/index.js index 8503d868..e5d34612 100644 --- a/src/lambda-canary-notification/index.js +++ b/src/lambda-canary-notification/index.js @@ -235,7 +235,7 @@ async function getRelevantPrefix(target, s3Client = new S3Client()) { * @param {array} keys - Array of objects representing each of the keys in the s3 folder/prefix * @returns {array} - Arrays of strings representing the keys of the failed page snapshots. */ -function extractScreenshotKeys(keys) { +function extractFailedScreenshotKeys(keys) { return keys .filter(item => item.Key.includes('-succeeded') && item.Key.endsWith('.png')) .map(item => item.Key); @@ -404,7 +404,7 @@ async function handler(event) { const slackPayload = buildSlackPostPayload(report.canaryName, report.startTime, report.endTime,) const slackPostResponse = await sendSlackPost(slackClient, slackPayload, slackSecret.slack_channel_id,) - const extractedSnapshotKeys = extractScreenshotKeys(folderContents) + const extractedSnapshotKeys = extractFailedScreenshotKeys(folderContents) const downloadResponses = await downloadAllFiles(extractedSnapshotKeys, S3_CANARY_LOGS_BUCKET_NAME) await uploadAllScreenshotsToSlackThread(downloadResponses, slackClient, slackSecret.slack_channel_id, slackPostResponse.ts) From 5cc4671c40778a33b329d2e238e886f030e7a150 Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Mon, 5 Aug 2024 19:31:16 +0100 Subject: [PATCH 25/59] Filter for failed snapshots only --- src/lambda-canary-notification/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lambda-canary-notification/index.js b/src/lambda-canary-notification/index.js index e5d34612..e60af1d2 100644 --- a/src/lambda-canary-notification/index.js +++ b/src/lambda-canary-notification/index.js @@ -237,7 +237,7 @@ async function getRelevantPrefix(target, s3Client = new S3Client()) { */ function extractFailedScreenshotKeys(keys) { return keys - .filter(item => item.Key.includes('-succeeded') && item.Key.endsWith('.png')) + .filter(item => item.Key.includes('-failed') && item.Key.endsWith('.png')) .map(item => item.Key); } From 13ead0c8d099d850a17cac0361554fd33a2875ec Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Thu, 8 Aug 2024 10:59:53 +0100 Subject: [PATCH 26/59] Extract and return broken links as bullet point list in post to Slack --- src/lambda-canary-notification/index.js | 49 ++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/src/lambda-canary-notification/index.js b/src/lambda-canary-notification/index.js index e60af1d2..4593fa8e 100644 --- a/src/lambda-canary-notification/index.js +++ b/src/lambda-canary-notification/index.js @@ -283,6 +283,7 @@ function buildSlackPostPayload(target, startTime, endTime) { "text": `*Alarm name:*\n${target}` } }, + buildBrokenLinksList(brokenLinks), { "type": "context", "elements": [ @@ -291,10 +292,56 @@ function buildSlackPostPayload(target, startTime, endTime) { "text": `Canary started at ${startTime} and failed at ${endTime}` } ] - } + }, ] } +/** + * Builds the `blocks` to be sent to the Slack API for the broken links bullet point list + * + * @param {array} brokenLinks - Array of strings, each of which represents a broken link + * + * @returns {object} - JSON object which can be used to post to the Slack channel with. + */ +function buildBrokenLinksList(brokenLinks) { + const blocks = { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + { + "type": "text", + "text": "Detected broken link(s):\n" + }, + ] + }, + { + "type": "rich_text_list", + "style": "bullet", + "indent": 0, + "border": 0, + "elements": [] + } + ] + } + brokenLinks.forEach(brokenLink => { + blocks.elements[1].elements.push( + { + "type": "rich_text_section", + "elements": [ + { + "type": "link", + "url": brokenLink + } + ] + }, + ); + }); + + return blocks +} + /** * Uploads the downloaded file to the given Slack channel under the parent thread as an individual reply. * From 2985011d9c6d6a9157f91b37cba522138a0976ab Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Thu, 8 Aug 2024 11:00:22 +0100 Subject: [PATCH 27/59] Pass `brokenLinks` array to `buildSlackPostPayload()` --- src/lambda-canary-notification/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lambda-canary-notification/index.js b/src/lambda-canary-notification/index.js index 4593fa8e..caf9ce2b 100644 --- a/src/lambda-canary-notification/index.js +++ b/src/lambda-canary-notification/index.js @@ -260,10 +260,11 @@ function extractReportKey(keys) { * @param {string} target - The name of the canary from which the alarm was raised. * @param {string} startTime - The time which the canary started its run. * @param {string} endTime - The time which the canary completed its run. + * @param {array} brokenLinks - Array of strings, each of which represents a broken link * * @returns {array} - An array of JSON objects which can be used to post to the Slack channel with. */ -function buildSlackPostPayload(target, startTime, endTime) { +function buildSlackPostPayload(target, startTime, endTime, brokenLinks) { return [ { "type": "header", From f2d52199ed343f289e616a8c3d515296e56a5048 Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Thu, 8 Aug 2024 11:01:48 +0100 Subject: [PATCH 28/59] Extract syntheics report and broken links report from s3 --- src/lambda-canary-notification/index.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/lambda-canary-notification/index.js b/src/lambda-canary-notification/index.js index caf9ce2b..f5c267d3 100644 --- a/src/lambda-canary-notification/index.js +++ b/src/lambda-canary-notification/index.js @@ -242,14 +242,14 @@ function extractFailedScreenshotKeys(keys) { } /** - * Extracts the key associated with the `SyntheticsReport` file from the given keys + * Extracts the file associated with the given key from the given keys * * @param {array} keys - Array of objects representing each of the keys in the s3 folder/prefix - * @returns {string} - The key associated with the `SyntheticsReport` file + * @returns {string} - The key associated with the given `keyToSearchFor` */ -function extractReportKey(keys) { +function extractReportKey(keys, keyToSearchFor) { const reportKeys = keys - .filter(item => item.Key.includes('SyntheticsReport')) + .filter(item => item.Key.includes(keyToSearchFor)) .map(item => item.Key); return reportKeys[0] } @@ -451,6 +451,8 @@ async function handler(event) { const report = await extractReport(folderContents) const slackPayload = buildSlackPostPayload(report.canaryName, report.startTime, report.endTime,) const slackPostResponse = await sendSlackPost(slackClient, slackPayload, slackSecret.slack_channel_id,) + const syntheticsReport = await extractReport(folderContents, 'SyntheticsReport') + const brokenLinksReport = await extractReport(folderContents, 'BrokenLinkCheckerReport') const extractedSnapshotKeys = extractFailedScreenshotKeys(folderContents) From 23354f3e1839ecca6363d8e864696a563cbdefe0 Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Thu, 8 Aug 2024 11:05:04 +0100 Subject: [PATCH 29/59] Add `keyToSearchFor` param to docstring --- src/lambda-canary-notification/index.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/lambda-canary-notification/index.js b/src/lambda-canary-notification/index.js index f5c267d3..aa5a871e 100644 --- a/src/lambda-canary-notification/index.js +++ b/src/lambda-canary-notification/index.js @@ -245,6 +245,8 @@ function extractFailedScreenshotKeys(keys) { * Extracts the file associated with the given key from the given keys * * @param {array} keys - Array of objects representing each of the keys in the s3 folder/prefix + * @param {string} keyToSearchFor - The key to filter for in the given `keys` + * * @returns {string} - The key associated with the given `keyToSearchFor` */ function extractReportKey(keys, keyToSearchFor) { @@ -397,10 +399,12 @@ async function sendSlackPost(slackClient, payload, channelId) { } /** - * Extracts the contents of the `SyntheticsReport` file from the given `folderContents` + * Extracts the contents of the report file from the given `folderContents` * * @param {array} folderContents - Array of objects representing the listed folder contents - * @returns {object} - The JSON representation of the contents of the `SyntheticReports` file. + * @param {string} keyToSearchFor - The key to filter for in the given `keys` + * + * @returns {object} - The JSON representation of the contents of the report file. */ async function extractReport(folderContents) { const downloadedReportKey = extractReportKey(folderContents) From e42245bde2afa2bb00b81abc29d2056738c06b98 Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Thu, 8 Aug 2024 11:05:16 +0100 Subject: [PATCH 30/59] Remove unused import --- src/lambda-canary-notification/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lambda-canary-notification/index.js b/src/lambda-canary-notification/index.js index aa5a871e..445924da 100644 --- a/src/lambda-canary-notification/index.js +++ b/src/lambda-canary-notification/index.js @@ -5,7 +5,7 @@ const { ListObjectsV2CommandOutput, GetObjectCommandOutput } = require("@aws-sdk/client-s3"); -const {SecretsManagerClient, GetSecretValueCommand, GetSecretValueCommandOutput} = require("@aws-sdk/client-secrets-manager"); +const {SecretsManagerClient, GetSecretValueCommand} = require("@aws-sdk/client-secrets-manager"); const {WebClient} = require("@slack/web-api") const axios = require('axios'); const FormData = require('form-data'); From 17236a56e95eab5dec35988c6a19464867ebaf09 Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Thu, 8 Aug 2024 11:05:47 +0100 Subject: [PATCH 31/59] Pass `keyToSearchFor` down into `extractReportKey()` --- src/lambda-canary-notification/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lambda-canary-notification/index.js b/src/lambda-canary-notification/index.js index 445924da..d14948f7 100644 --- a/src/lambda-canary-notification/index.js +++ b/src/lambda-canary-notification/index.js @@ -406,9 +406,9 @@ async function sendSlackPost(slackClient, payload, channelId) { * * @returns {object} - The JSON representation of the contents of the report file. */ -async function extractReport(folderContents) { - const downloadedReportKey = extractReportKey(folderContents) - const downloadedReportResponse = await downloadFile(S3_CANARY_LOGS_BUCKET_NAME, downloadedReportKey,) +async function extractReport(folderContents, keyToSearchFor) { + const downloadedReportKey = extractReportKey(folderContents, keyToSearchFor) + const downloadedReportResponse = await downloadFile(S3_CANARY_LOGS_BUCKET_NAME, downloadedReportKey) const reportFileBuffer = await streamToBuffer(downloadedReportResponse.Body); From 95ec428c55666bd086c5b447611370f9863495f6 Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Thu, 8 Aug 2024 11:06:11 +0100 Subject: [PATCH 32/59] Extract synthetics report and broken links report to build slack post payload --- src/lambda-canary-notification/index.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/lambda-canary-notification/index.js b/src/lambda-canary-notification/index.js index d14948f7..6ba2ace1 100644 --- a/src/lambda-canary-notification/index.js +++ b/src/lambda-canary-notification/index.js @@ -452,9 +452,6 @@ async function handler(event) { const listedFiles = await listFiles(S3_CANARY_LOGS_BUCKET_NAME, relevantFolder) const folderContents = listedFiles.Contents - const report = await extractReport(folderContents) - const slackPayload = buildSlackPostPayload(report.canaryName, report.startTime, report.endTime,) - const slackPostResponse = await sendSlackPost(slackClient, slackPayload, slackSecret.slack_channel_id,) const syntheticsReport = await extractReport(folderContents, 'SyntheticsReport') const brokenLinksReport = await extractReport(folderContents, 'BrokenLinkCheckerReport') From 612eb89ba8a4587e4411429f8772aa217f44118f Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Thu, 8 Aug 2024 11:06:24 +0100 Subject: [PATCH 33/59] Formatting --- src/lambda-canary-notification/index.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/lambda-canary-notification/index.js b/src/lambda-canary-notification/index.js index 6ba2ace1..9b15dcc4 100644 --- a/src/lambda-canary-notification/index.js +++ b/src/lambda-canary-notification/index.js @@ -455,6 +455,14 @@ async function handler(event) { const syntheticsReport = await extractReport(folderContents, 'SyntheticsReport') const brokenLinksReport = await extractReport(folderContents, 'BrokenLinkCheckerReport') + const slackPayload = buildSlackPostPayload( + syntheticsReport.canaryName, + syntheticsReport.startTime, + syntheticsReport.endTime, + brokenLinksReport.brokenLinks + ) + const slackPostResponse = await sendSlackPost(slackClient, slackPayload, slackSecret.slack_channel_id) + const extractedSnapshotKeys = extractFailedScreenshotKeys(folderContents) const downloadResponses = await downloadAllFiles(extractedSnapshotKeys, S3_CANARY_LOGS_BUCKET_NAME) From 077e670b52944857954ad58613f8028be2c005de Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Thu, 8 Aug 2024 11:06:45 +0100 Subject: [PATCH 34/59] Relaunch browser after 50 pages --- src/canary-front-end-broken-links/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/canary-front-end-broken-links/index.js b/src/canary-front-end-broken-links/index.js index 5605dda2..ee7907b8 100644 --- a/src/canary-front-end-broken-links/index.js +++ b/src/canary-front-end-broken-links/index.js @@ -44,7 +44,7 @@ const captureDestinationPageScreenshotOnFailure = true; // Close and Re-launch browser after checking these many links. This clears up /tmp disk storage occupied by chromium and launches a new browser for next set of links. // Increase or decrease based on complexity of your website. -const numOfLinksToReLaunchBrowser = 20; +const numOfLinksToReLaunchBrowser = 50; // async function used to grab urls from page // fetch hrefs from DOM From f8af73b55a96064aade699818fab5cc9b8ec02a8 Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Fri, 9 Aug 2024 10:30:56 +0100 Subject: [PATCH 35/59] Remove unused `slack/webhook` dependency --- src/lambda-canary-notification/package-lock.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lambda-canary-notification/package-lock.json b/src/lambda-canary-notification/package-lock.json index 669e1df3..946ba64f 100644 --- a/src/lambda-canary-notification/package-lock.json +++ b/src/lambda-canary-notification/package-lock.json @@ -11,7 +11,6 @@ "dependencies": { "@aws-sdk/client-s3": "^3.556.0", "@slack/web-api": "^7.3.1", - "@slack/webhook": "^7.0.2", "axios": "^1.7.2", "form-data": "^4.0.0" }, From 43508f44808bd81b35b85e3d203a22b41e105c8b Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Fri, 9 Aug 2024 10:31:15 +0100 Subject: [PATCH 36/59] Add `aws-sdk/client-secrets-manager` to dependencies --- .../package-lock.json | 299 +++++++++++++++++- src/lambda-canary-notification/package.json | 2 +- 2 files changed, 286 insertions(+), 15 deletions(-) diff --git a/src/lambda-canary-notification/package-lock.json b/src/lambda-canary-notification/package-lock.json index 946ba64f..79cba15e 100644 --- a/src/lambda-canary-notification/package-lock.json +++ b/src/lambda-canary-notification/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@aws-sdk/client-s3": "^3.556.0", + "@aws-sdk/client-secrets-manager": "^3.624.0", "@slack/web-api": "^7.3.1", "axios": "^1.7.2", "form-data": "^4.0.0" @@ -287,6 +288,290 @@ "node": ">=16.0.0" } }, + "node_modules/@aws-sdk/client-secrets-manager": { + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.624.0.tgz", + "integrity": "sha512-sW4eT+OVhfMTTB9Ke5tAz8/1gZmJ4G40z9Pvm4fJYRopIMIkHSeSQKTo5urX0APYZ3fdKs2Hxo22MKIZAO4kmw==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.624.0", + "@aws-sdk/client-sts": "3.624.0", + "@aws-sdk/core": "3.624.0", + "@aws-sdk/credential-provider-node": "3.624.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.620.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.2", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.14", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/client-sso": { + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.624.0.tgz", + "integrity": "sha512-EX6EF+rJzMPC5dcdsu40xSi2To7GSvdGQNIpe97pD9WvZwM9tRNQnNM4T6HA4gjV1L6Jwk8rBlG/CnveXtLEMw==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.624.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.620.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.2", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.14", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.624.0.tgz", + "integrity": "sha512-Ki2uKYJKKtfHxxZsiMTOvJoVRP6b2pZ1u3rcUb2m/nVgBPUfLdl8ZkGpqE29I+t5/QaS/sEdbn6cgMUZwl+3Dg==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.624.0", + "@aws-sdk/credential-provider-node": "3.624.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.620.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.2", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.14", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.624.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/client-sts": { + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.624.0.tgz", + "integrity": "sha512-k36fLZCb2nfoV/DKK3jbRgO/Yf7/R80pgYfMiotkGjnZwDmRvNN08z4l06L9C+CieazzkgRxNUzyppsYcYsQaw==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.624.0", + "@aws-sdk/core": "3.624.0", + "@aws-sdk/credential-provider-node": "3.624.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.620.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.2", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.14", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/core": { + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.624.0.tgz", + "integrity": "sha512-WyFmPbhRIvtWi7hBp8uSFy+iPpj8ccNV/eX86hwF4irMjfc/FtsGVIAeBXxXM/vGCjkdfEzOnl+tJ2XACD4OXg==", + "dependencies": { + "@smithy/core": "^2.3.2", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/signature-v4": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/util-middleware": "^3.0.3", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.624.0.tgz", + "integrity": "sha512-mMoNIy7MO2WTBbdqMyLpbt6SZpthE6e0GkRYpsd0yozPt0RZopcBhEh+HG1U9Y1PVODo+jcMk353vAi61CfnhQ==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.620.1", + "@aws-sdk/credential-provider-http": "3.622.0", + "@aws-sdk/credential-provider-process": "3.620.1", + "@aws-sdk/credential-provider-sso": "3.624.0", + "@aws-sdk/credential-provider-web-identity": "3.621.0", + "@aws-sdk/types": "3.609.0", + "@smithy/credential-provider-imds": "^3.2.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.624.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.624.0.tgz", + "integrity": "sha512-vYyGK7oNpd81BdbH5IlmQ6zfaQqU+rPwsKTDDBeLRjshtrGXOEpfoahVpG9PX0ibu32IOWp4ZyXBNyVrnvcMOw==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.620.1", + "@aws-sdk/credential-provider-http": "3.622.0", + "@aws-sdk/credential-provider-ini": "3.624.0", + "@aws-sdk/credential-provider-process": "3.620.1", + "@aws-sdk/credential-provider-sso": "3.624.0", + "@aws-sdk/credential-provider-web-identity": "3.621.0", + "@aws-sdk/types": "3.609.0", + "@smithy/credential-provider-imds": "^3.2.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.624.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.624.0.tgz", + "integrity": "sha512-A02bayIjU9APEPKr3HudrFHEx0WfghoSPsPopckDkW7VBqO4wizzcxr75Q9A3vNX+cwg0wCN6UitTNe6pVlRaQ==", + "dependencies": { + "@aws-sdk/client-sso": "3.624.0", + "@aws-sdk/token-providers": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@aws-sdk/client-sso": { "version": "3.623.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.623.0.tgz", @@ -1847,20 +2132,6 @@ "npm": ">= 8.6.0" } }, - "node_modules/@slack/webhook": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@slack/webhook/-/webhook-7.0.2.tgz", - "integrity": "sha512-dsrO/ow6a6+xkLm/lZKbUNTsFJlBc679tD+qwlVTztsQkDxPLH6odM7FKALz1IHa+KpLX8HKUIPV13a7y7z29w==", - "dependencies": { - "@slack/types": "^2.9.0", - "@types/node": ">=18.0.0", - "axios": "^1.6.3" - }, - "engines": { - "node": ">= 18", - "npm": ">= 8.6.0" - } - }, "node_modules/@smithy/abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.1.tgz", diff --git a/src/lambda-canary-notification/package.json b/src/lambda-canary-notification/package.json index 5bd5dab4..f2b91e7e 100644 --- a/src/lambda-canary-notification/package.json +++ b/src/lambda-canary-notification/package.json @@ -16,7 +16,7 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.556.0", - "@slack/webhook": "^7.0.2", + "@aws-sdk/client-secrets-manager": "^3.624.0", "@slack/web-api": "^7.3.1", "axios": "^1.7.2", "form-data": "^4.0.0" From 74d95744afb99a2fd20c60d597c16676bf9a296e Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Fri, 9 Aug 2024 14:27:07 +0100 Subject: [PATCH 37/59] Rename arg --- src/lambda-canary-notification/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lambda-canary-notification/index.js b/src/lambda-canary-notification/index.js index 9b15dcc4..427df347 100644 --- a/src/lambda-canary-notification/index.js +++ b/src/lambda-canary-notification/index.js @@ -117,11 +117,11 @@ async function downloadAllFiles(keys, bucket, s3Client = new S3Client()) { /** * Sets up the `WebClient` object used to interact with Slack. * - * @param {string} slack_token - The token associated with the Slack bot. + * @param {string} token - The token associated with the Slack bot. * @returns {WebClient} - An instantiated `WebClient` instance used to interact with Slack. */ -function buildSlackClient(slack_token) { - return new WebClient(slack_token); +function buildSlackClient(token) { + return new WebClient(token); } /** From 66534dd7a726871227c8cb7abf992e1c5631f7a0 Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Fri, 9 Aug 2024 14:27:51 +0100 Subject: [PATCH 38/59] Inject dependencies into all functions which depend on side-effect-heavy functions --- src/lambda-canary-notification/index.js | 165 ++++++++++++++++++++---- 1 file changed, 137 insertions(+), 28 deletions(-) diff --git a/src/lambda-canary-notification/index.js b/src/lambda-canary-notification/index.js index 427df347..2cb1edef 100644 --- a/src/lambda-canary-notification/index.js +++ b/src/lambda-canary-notification/index.js @@ -352,20 +352,50 @@ function buildBrokenLinksList(brokenLinks) { * @param {WebClient} slackClient - The `WebClient` instance used to interact with Slack. * @param {string} channelId - The ID associated with the channel which the image is being sent to. * @param {string} threadTs - The ID of the parent thread to attach this image as a reply to. + * @param overriddenDependencies - Object used to override the default dependencies. * * @returns {Promise} - The promise from the function call */ -async function uploadScreenshotToSlackThread(downloadedFileResponse, slackClient, channelId, threadTs) { - const fileBufferStream = await streamToBuffer(downloadedFileResponse.content.Body); - const fileName = getFilename(downloadedFileResponse.key); - const fileUploadURLResponse = await getFileUploadURLToSlack(slackClient, fileName, fileBufferStream.length) +async function uploadScreenshotToSlackThread( + downloadedFileResponse, + slackClient, + channelId, + threadTs, + overriddenDependencies = {} +) { + + const defaultDependencies = { + streamToBuffer, + getFilename, + getFileUploadURLToSlack, + sendPostRequest, + completeFileUploadToSlack + }; + const dependencies = {...defaultDependencies, ...overriddenDependencies} + + const fileBufferStream = await dependencies.streamToBuffer(downloadedFileResponse.content.Body); + const fileName = dependencies.getFilename(downloadedFileResponse.key); + const fileUploadURLResponse = await dependencies.getFileUploadURLToSlack( + slackClient, + fileName, + fileBufferStream.length + ) const uploadURL = fileUploadURLResponse.upload_url const fileID = fileUploadURLResponse.file_id - await sendPostRequest(uploadURL, fileName, fileBufferStream) + await dependencies.sendPostRequest( + uploadURL, + fileName, + fileBufferStream + ) const files = [{"id": fileID, "title": fileName}] - await completeFileUploadToSlack(slackClient, files, channelId, threadTs) + await dependencies.completeFileUploadToSlack( + slackClient, + files, + channelId, + threadTs + ) } /** @@ -375,12 +405,24 @@ async function uploadScreenshotToSlackThread(downloadedFileResponse, slackClient * @param {WebClient} slackClient - The `WebClient` instance used to interact with Slack. * @param {string} channelId - The ID associated with the channel which the image is being sent to. * @param {string} threadTs - The ID of the parent thread to attach this image as a reply to. + * @param overriddenDependencies - Object used to override the default dependencies. * * @returns {Promise} - The promise from the function call */ -async function uploadAllScreenshotsToSlackThread(downloadedFileResponses, slackClient, channelId, threadTs) { +async function uploadAllScreenshotsToSlackThread( + downloadedFileResponses, + slackClient, + channelId, + threadTs, + overriddenDependencies = {} +) { + const defaultDependencies = { + uploadScreenshotToSlackThread, + }; + const dependencies = {...defaultDependencies, ...overriddenDependencies} + for (let downloadedFileResponse of downloadedFileResponses) { - await uploadScreenshotToSlackThread(downloadedFileResponse, slackClient, channelId, threadTs) + await dependencies.uploadScreenshotToSlackThread(downloadedFileResponse, slackClient, channelId, threadTs) } } @@ -403,14 +445,31 @@ async function sendSlackPost(slackClient, payload, channelId) { * * @param {array} folderContents - Array of objects representing the listed folder contents * @param {string} keyToSearchFor - The key to filter for in the given `keys` + * @param {string} bucketName - The name of the S3 bucket to search in. + * Defaults to the env var `S3_CANARY_LOGS_BUCKET_NAME` + * @param {S3Client} s3Client - An optional instance of the S3Client to use for downloading the file. + * @param overriddenDependencies - Object used to override the default dependencies. * * @returns {object} - The JSON representation of the contents of the report file. */ -async function extractReport(folderContents, keyToSearchFor) { - const downloadedReportKey = extractReportKey(folderContents, keyToSearchFor) - const downloadedReportResponse = await downloadFile(S3_CANARY_LOGS_BUCKET_NAME, downloadedReportKey) +async function extractReport( + folderContents, + keyToSearchFor, + bucketName = S3_CANARY_LOGS_BUCKET_NAME, + s3Client = new S3Client(), + overriddenDependencies = {} +) { + const defaultDependencies = { + extractReportKey, + downloadFile, + streamToBuffer, + }; + const dependencies = {...defaultDependencies, ...overriddenDependencies} - const reportFileBuffer = await streamToBuffer(downloadedReportResponse.Body); + const downloadedReportKey = dependencies.extractReportKey(folderContents, keyToSearchFor) + const downloadedReportResponse = await dependencies.downloadFile(bucketName, downloadedReportKey, s3Client) + + const reportFileBuffer = await dependencies.streamToBuffer(downloadedReportResponse.Body); const jsonString = reportFileBuffer.toString('utf8'); return JSON.parse(jsonString); @@ -431,44 +490,94 @@ function extractTargetFromEvent(event) { * Calculates the relevant folder in s3 relating to the triggered Canary results. * * @param {object} event - The object passed down to the Lambda runtime on initialization. + * @param overriddenDependencies - Object used to override the default dependencies. + * * @returns {string} - The prefix associated with the 'folder' of the triggered Canary results. */ -async function determineRelevantFolderInS3(event) { - const target = extractTargetFromEvent(event) - return await getRelevantPrefix(target) +async function determineRelevantFolderInS3(event, overriddenDependencies = {}) { + const defaultDependencies = { + extractTargetFromEvent, + getRelevantPrefix, + }; + const dependencies = {...defaultDependencies, ...overriddenDependencies} + + const target = dependencies.extractTargetFromEvent(event) + return await dependencies.getRelevantPrefix(target) } /** * Main handler entrypoint for the Lambda runtime execution. * * @param {object} event - The object passed down to the Lambda runtime on initialization. + * @param {string} bucketName - The name of the S3 bucket to search in. + * Defaults to the env var `S3_CANARY_LOGS_BUCKET_NAME` + * @param overriddenDependencies - Object used to override the default dependencies. + * */ -async function handler(event) { - const relevantFolder = await determineRelevantFolderInS3(event) - const slackSecret = await getSlackSecret() +async function handler(event, bucketName = S3_CANARY_LOGS_BUCKET_NAME, overriddenDependencies = {}) { + const defaultDependencies = { + determineRelevantFolderInS3, + getSlackSecret, + buildSlackClient, + listFiles, + extractReport, + buildSlackPostPayload, + sendSlackPost, + extractFailedScreenshotKeys, + downloadAllFiles, + uploadAllScreenshotsToSlackThread + }; + const dependencies = {...defaultDependencies, ...overriddenDependencies} + + const slackSecret = await dependencies.getSlackSecret() + const slackClient = await dependencies.buildSlackClient(slackSecret.slack_token) - const slackClient = await buildSlackClient(slackSecret.slack_token) + const relevantFolder = await dependencies.determineRelevantFolderInS3(event) - const listedFiles = await listFiles(S3_CANARY_LOGS_BUCKET_NAME, relevantFolder) + const listedFiles = await dependencies.listFiles(bucketName, relevantFolder) const folderContents = listedFiles.Contents - const syntheticsReport = await extractReport(folderContents, 'SyntheticsReport') - const brokenLinksReport = await extractReport(folderContents, 'BrokenLinkCheckerReport') + const syntheticsReport = await dependencies.extractReport(folderContents, 'SyntheticsReport') + const brokenLinksReport = await dependencies.extractReport(folderContents, 'BrokenLinkCheckerReport') - const slackPayload = buildSlackPostPayload( + const slackPayload = dependencies.buildSlackPostPayload( syntheticsReport.canaryName, syntheticsReport.startTime, syntheticsReport.endTime, brokenLinksReport.brokenLinks ) - const slackPostResponse = await sendSlackPost(slackClient, slackPayload, slackSecret.slack_channel_id) + const slackPostResponse = await dependencies.sendSlackPost(slackClient, slackPayload, slackSecret.slack_channel_id) - const extractedSnapshotKeys = extractFailedScreenshotKeys(folderContents) + const extractedSnapshotKeys = dependencies.extractFailedScreenshotKeys(folderContents) + const downloadResponses = await dependencies.downloadAllFiles(extractedSnapshotKeys, bucketName) - const downloadResponses = await downloadAllFiles(extractedSnapshotKeys, S3_CANARY_LOGS_BUCKET_NAME) - await uploadAllScreenshotsToSlackThread(downloadResponses, slackClient, slackSecret.slack_channel_id, slackPostResponse.ts) + await dependencies.uploadAllScreenshotsToSlackThread(downloadResponses, slackClient, slackSecret.slack_channel_id, slackPostResponse.ts) } module.exports = { - handler + handler, + getFilename, + getSecret, + getSlackSecret, + listFiles, + streamToBuffer, + downloadFile, + downloadAllFiles, + buildSlackClient, + getFileUploadURLToSlack, + sendPostRequest, + completeFileUploadToSlack, + getDirectoryPath, + getCurrentDate, + getRelevantPrefix, + extractFailedScreenshotKeys, + extractReportKey, + buildSlackPostPayload, + buildBrokenLinksList, + uploadScreenshotToSlackThread, + uploadAllScreenshotsToSlackThread, + sendSlackPost, + extractReport, + extractTargetFromEvent, + determineRelevantFolderInS3, } \ No newline at end of file From a07e3eb9daf6060c85f19b104da74c6bfac8a567 Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Fri, 9 Aug 2024 14:28:20 +0100 Subject: [PATCH 39/59] Add unit tests --- src/lambda-canary-notification/index.test.js | 822 +++++++++++++++++++ 1 file changed, 822 insertions(+) create mode 100644 src/lambda-canary-notification/index.test.js diff --git a/src/lambda-canary-notification/index.test.js b/src/lambda-canary-notification/index.test.js new file mode 100644 index 00000000..5dcf6df2 --- /dev/null +++ b/src/lambda-canary-notification/index.test.js @@ -0,0 +1,822 @@ +const {GetObjectCommand, ListObjectsV2Command} = require("@aws-sdk/client-s3"); +const axios = require('axios'); +const FormData = require('form-data'); +const path = require('path'); +const sinon = require("sinon"); +const {GetSecretValueCommand} = require("@aws-sdk/client-secrets-manager"); +const index = require("./index"); + +// Mock dependencies +jest.mock('axios'); +jest.mock('form-data'); +jest.mock('path'); + +describe('getFilename', () => { + /** + * Given a file path + * When `getFilename` is called + * Then it should return the correct file name + */ + test('Extracts the filename from the file path', () => { + // Given + const filePath = 'folder/subfolder/02-covid-19-failed.png'; + path.basename.mockReturnValue('02-covid-19-failed.png'); + + // When + const result = index.getFilename(filePath); + + // Then + expect(result).toBe('02-covid-19-failed.png'); + expect(path.basename).toHaveBeenCalledWith(filePath); + }); +}); + +describe('getSecret', () => { + /** + * Given the ARN of the secret associated with the Slack webhook URL + * When `getSecret()` is called + * Then the correct command is used when + * the `send` method is called from the `SecretsManagerClient` + */ + test('Gets the secret from SecretsManager', async () => { + // Given + const fakeSecretARN = 'fake-arn-for-secret'; + const mockedEnvVar = sinon.stub(process, 'env').value({SECRETS_MANAGER_SLACK_WEBHOOK_URL_ARN: fakeSecretARN}); + const spySecretsManagerClient = { + send: sinon.stub().resolves({}), + } + + // When + await index.getSecret(spySecretsManagerClient); + + // Then + expect(spySecretsManagerClient.send.calledWith(sinon.match.instanceOf(GetSecretValueCommand))).toBeTruthy() + const argsCalledWithSpy = spySecretsManagerClient.send.firstCall.args[0].input; + expect(argsCalledWithSpy.SecretId).toEqual(fakeSecretARN); + // Restore the environment variable + mockedEnvVar.restore(); + }); +}); + +describe('getSlackSecret', () => { + /** + * Given a mocked SecretsManagerClient instance + * When `getSlackSecret()` is called + * Then it should return the parsed JSON secret + */ + test('Gets and parses the Slack secret from SecretsManager', async () => { + // Given + const mockedResponse = {SecretString: '{"slack_token": "test-token"}'}; + const mockedSecretsManagerClient = { + send: sinon.stub().resolves(mockedResponse), + } + + // When + const result = await index.getSlackSecret(mockedSecretsManagerClient); + + // Then + expect(result).toEqual({slack_token: 'test-token'}); + }); +}); + +describe('listFiles', () => { + /** + * Given an S3 bucket name and prefix to filter files b + * When `listFiles()` is called + * Then the correct command is used when + * the `send` method is called from the `S3Client` + */ + test('Lists files in S3 bucket with the specified prefix', async () => { + // Given + const bucketName = 'test-bucket' + const prefix = 'test-prefix' + const spyS3Client = { + send: sinon.stub().resolves({}), + } + + // When + await index.listFiles(bucketName, prefix, spyS3Client); + + // Then + expect(spyS3Client.send.calledWith(sinon.match.instanceOf(ListObjectsV2Command))).toBeTruthy() + const argsCalledWithSpy = spyS3Client.send.firstCall.args[0].input; + expect(argsCalledWithSpy.Bucket).toEqual(bucketName); + expect(argsCalledWithSpy.Prefix).toEqual(prefix); + }); +}); + +describe('streamToBuffer', () => { + /** + * Given a stream + * When `streamToBuffer()` is called + * Then it should return the buffer created from the stream data + */ + test('Converts a stream to a buffer', async () => { + // Given + const mockStream = { + on: jest.fn((event, callback) => { + if (event === 'data') callback(Buffer.from('chunk')); + if (event === 'end') callback(); + }), + }; + + // When + const result = await index.streamToBuffer(mockStream); + + // Then + expect(result).toEqual(Buffer.from('chunk')); + }); +}); + +describe('downloadFile', () => { + /** + * Given an S3 bucket and key + * When `downloadFile()` is called + * Then it should return the file from S3 + */ + test('Downloads the file from S3', async () => { + // Given + const bucketName = 'test-bucket' + const key = 'test-key' + const mockedResponse = {Body: 'fileContent'}; + const spyS3Client = { + send: sinon.stub().resolves(mockedResponse), + } + + // When + const result = await index.downloadFile(bucketName, key, spyS3Client); + + // Then + expect(spyS3Client.send.calledWith(sinon.match.instanceOf(GetObjectCommand))).toBeTruthy() + const argsCalledWithSpy = spyS3Client.send.firstCall.args[0].input; + expect(argsCalledWithSpy.Bucket).toEqual(bucketName); + expect(argsCalledWithSpy.Key).toEqual(key); + expect(result).toBe(mockedResponse); + }); +}); + +describe('downloadAllFiles', () => { + /** + * Given an S3 bucket and multiple keys + * When `downloadAllFiles()` is called + * Then it should return all the files from S3 + */ + test('Downloads all files from S3', async () => { + // Given + const keys = ['key1', 'key2'] + const bucketName = 'test-bucket' + const mockedResponse = {Body: 'fileContent'}; + const spyS3Client = { + send: sinon.stub().resolves(mockedResponse), + } + + // When + const result = await index.downloadAllFiles(keys, bucketName, spyS3Client); + + // Then + expect(result).toEqual([{key: 'key1', content: mockedResponse}, {key: 'key2', content: mockedResponse},]); + expect(spyS3Client.send.calledTwice).toBeTruthy(); + }); +}); + +describe('getFileUploadURLToSlack', () => { + /** + * Given a `WebClient`, filename, and length + * When `getFileUploadURLToSlack()` is called + * Then it should call `getUploadURLExternal` on the client with correct arguments + */ + test('Calls `getUploadURLExternal` with correct arguments and returns result', async () => { + // Given + const mockedSlackClient = { + files: { + getUploadURLExternal: sinon.stub(), + }, token: 'fake-token', + }; + const filename = '01-covid-19-failed.png'; + const length = 1024; + const mockResponse = {upload_url: 'https://slack.com/upload', file_id: 'abc123'}; + mockedSlackClient.files.getUploadURLExternal.resolves(mockResponse); + + // When + const result = await index.getFileUploadURLToSlack(mockedSlackClient, filename, length); + + // Then + sinon.assert.calledOnceWithExactly(mockedSlackClient.files.getUploadURLExternal, { + token: mockedSlackClient.token, filename: filename, length: length, + }); + expect(result).toEqual(mockResponse); + }); +}); + +describe('sendPostRequest', () => { + /** + * Given a URL, filename, and file buffer stream + * When `sendPostRequest()` is called + * Then it should send a POST request with the file + */ + test('Sends a POST request to the given URL', async () => { + // Given + const fakeURL = 'https://slack.com/upload'; + const fakeFilename = '01-covid-19-failed.png'; + const fakeFileBufferStream = Buffer.from('fileContent'); + axios.post.mockResolvedValue({status: 200}); + + // When + await index.sendPostRequest(fakeURL, fakeFilename, fakeFileBufferStream); + + // Then + expect(axios.post).toHaveBeenCalledWith(fakeURL, expect.any(FormData), { + headers: expect.any(Object), + }); + }); +}); + +describe('completeFileUploadToSlack', () => { + /** + * Given a mocked `WebClient`, file details, channel ID, and thread timestamp + * When `completeFileUploadToSlack()` is called + * Then it should call `completeUploadExternal` on the Slack client with correct arguments + */ + test('Completes a file upload in Slack', async () => { + // Given + const mockSlackClient = { + files: { + completeUploadExternal: sinon.stub().resolves() + }, token: 'fake-token', + }; + const files = [{id: 'file_id', title: 'file.txt'}]; + const channelId = 'C123'; + const threadTs = 'thread_ts'; + + // When + await index.completeFileUploadToSlack(mockSlackClient, files, channelId, threadTs); + + // Then + sinon.assert.calledOnceWithExactly(mockSlackClient.files.completeUploadExternal, { + token: mockSlackClient.token, files: files, channel_id: channelId, thread_ts: threadTs, + }); + }); +}); + +describe('getDirectoryPath', () => { + /** + * Given a full file path + * When `getDirectoryPath()` is called + * Then the directory path is returned + */ + test('Returns the directory path for the prefix', () => { + // Given + const prefix = 'canary/eu-west-2/test-env-display/2024/08/08/15-02-01'; + const filePath = `${prefix}/01-covid-19-failed.png` + + // When + const result = index.getDirectoryPath(filePath); + + // Then + expect(result).toBe(prefix); + }); +}); + +describe('getCurrentDate', () => { + /** + * Given no input + * When `getCurrentDate()` is called + * Then it should return the current date in YYYY/MM/DD format + */ + test('Returns the current date in YYYY/MM/DD format', () => { + // Given + // Date is mocked to avoid any potential flakiness + const frozenDate = new Date('2023-12-25T00:00:00Z'); + jest.spyOn(global, 'Date').mockImplementation(() => frozenDate); + + // When + const result = index.getCurrentDate(); + + // Then + const expectedDate = { + 'year': '2023', 'month': '12', 'day': '25', 'hour': '00' + } + expect(result).toStrictEqual(expectedDate); + }); +}); + +describe('getRelevantPrefix', () => { + /** + * Given a failed screenshot keys array and prefix + * When `getRelevantPrefix()` is called + * Then it should return the relevant prefix + */ + test('Returns the relevant prefix based on failed screenshot keys', async () => { + // Given + const frozenDate = new Date('2023-12-25T00:00:00Z'); + jest.spyOn(global, 'Date').mockImplementation(() => frozenDate); + const fakeTarget = 'uhd-fake-target'; + const fakeDataFromS3 = { + Contents: [ + {Key: `canary/eu-west-2/${fakeTarget}/2023/12/07/08/item.png`}, + {Key: `canary/eu-west-2/${fakeTarget}/2023/12/19/14/report.json`}, + {Key: `canary/eu-west-2/${fakeTarget}/2023/12/25/00/another-item.png`}, + ] + } + const mockedS3Client = { + send: sinon.stub().resolves(fakeDataFromS3), + }; + + // When + const result = await index.getRelevantPrefix(fakeTarget, mockedS3Client); + + // Then + expect(result).toStrictEqual(`canary/eu-west-2/${fakeTarget}/2023/12/25/00`); + }); +}); + +describe('extractFailedScreenshotKeys', () => { + /** + * Given an S3 file list + * When `extractFailedScreenshotKeys()` is called + * Then it should return the keys of failed screenshots + */ + test('Extracts keys of failed screenshots from S3 file list', () => { + // Given + const failedKey = 'canary/eu-west-2/test-env-display/2024/08/08/15-02-01/01-covid-19-failed.png' + const succeededKey = 'canary/eu-west-2/test-env-display/2024/08/08/15-02-01/02-influenza-succeeded.png' + const fakeKeys = [{Key: failedKey}, {Key: succeededKey},]; + + // When + const result = index.extractFailedScreenshotKeys(fakeKeys); + + // Then + expect(result).toEqual([failedKey]); + }); +}); + +describe('extractReportKey', () => { + /** + * Given an S3 file list + * When `extractReportKey()` is called + * Then it should return the key of the report + */ + test('Extracts the report key from S3 file list', () => { + // Given + const failedScreenshotKey = 'canary/eu-west-2/test-env-display/2024/08/08/15-02-01/01-covid-19-failed.png' + const BrokenKeyLinksReportKey = 'canary/eu-west-2/test-env-display/2024/08/08/15-02-01/BrokenLinkCheckerReport.json' + const fakeKeys = [{Key: failedScreenshotKey}, {Key: BrokenKeyLinksReportKey},]; + const keyToSearchFor = 'BrokenLinkCheckerReport' + + // When + const result = index.extractReportKey(fakeKeys, keyToSearchFor); + + // Then + expect(result).toBe(BrokenKeyLinksReportKey); + }); +}); + +describe('buildSlackPostPayload', () => { + /** + * Given a target, timestamps and an array of broken links + * When `buildSlackPostPayload()` is called + * Then it should return the payload for posting to Slack + */ + test('Builds the Slack post payload', () => { + // Given + const target = 'uhd-test-env-display' + const startTime = 'fake-start-time' + const endTime = 'fake-end-time' + const brokenLinks = ['fake-link-1.com', 'fake-link-2.com'] + + // When + const result = index.buildSlackPostPayload(target, startTime, endTime, brokenLinks); + + // Then + const expectedPayload = [{ + "type": "header", "text": { + "type": "plain_text", "text": ":alert: Canary run failed", "emoji": true + } + }, { + "type": "divider" + }, { + "type": "section", "text": { + "type": "mrkdwn", "text": `*Alarm name:*\n${target}` + } + }, { + "elements": [{ + "elements": [{ + "text": "Detected broken link(s):\n", "type": "text" + }], "type": "rich_text_section" + }, { + "border": 0, "elements": [{ + "elements": [{ + "type": "link", "url": "fake-link-1.com" + }], "type": "rich_text_section" + }, { + "elements": [{ + "type": "link", "url": "fake-link-2.com" + }], "type": "rich_text_section" + }], "indent": 0, "style": "bullet", "type": "rich_text_list" + }], "type": "rich_text" + }, { + "type": "context", "elements": [{ + "type": "plain_text", "text": `Canary started at ${startTime} and failed at ${endTime}` + }] + },] + expect(result).toEqual(expectedPayload); + }); +}); + +describe('uploadScreenshotToSlackThread', () => { + /** + * Given a Slack channel ID and thread ts value + * When `uploadScreenshotToSlackThread()` is called + * Then the call is delegated to the relevant functions + */ + it('should correctly upload a screenshot to a Slack thread', async () => { + // Given + const mockedSlackClient = sinon.stub(); + const fakeDownloadedFileResponse = {content: {Body: 'fake-body'}, key: 'fake-key'}; + const fakeChannelId = 'C123'; + const fakeThreadTs = 'fake_thread_ts_value'; + const fileBufferStream = Buffer.from('mock-buffer'); + const fakeFileName = 'fake-screenshot.png'; + const fakeFileUploadURLResponse = {upload_url: 'https://slack.com/upload', file_id: 'fake-file-id'}; + + const spyStreamToBuffer = sinon.stub().resolves(fileBufferStream); + const spyGetFilename = sinon.stub().returns(fakeFileName); + const spyGetFileUploadURLToSlack = sinon.stub().resolves(fakeFileUploadURLResponse); + const spySendPostRequest = sinon.stub(); + const spyCompleteFileUploadToSlack = sinon.stub(); + const overriddenDependencies = { + streamToBuffer: spyStreamToBuffer, + getFilename: spyGetFilename, + getFileUploadURLToSlack: spyGetFileUploadURLToSlack, + sendPostRequest: spySendPostRequest, + completeFileUploadToSlack: spyCompleteFileUploadToSlack + }; + + // When + await index.uploadScreenshotToSlackThread( + fakeDownloadedFileResponse, + mockedSlackClient, + fakeChannelId, + fakeThreadTs, + overriddenDependencies + ); + + // Then + expect(spyStreamToBuffer.calledOnceWithExactly(fakeDownloadedFileResponse.content.Body)).toBeTruthy(); + expect(spyGetFilename.calledOnceWithExactly(fakeDownloadedFileResponse.key)).toBeTruthy(); + expect(spyGetFileUploadURLToSlack.calledOnceWithExactly(mockedSlackClient, fakeFileName, fileBufferStream.length)).toBeTruthy(); + expect(spySendPostRequest.calledOnceWithExactly(fakeFileUploadURLResponse.upload_url, fakeFileName, fileBufferStream)).toBeTruthy(); + const expectedFiles = [{id: fakeFileUploadURLResponse.file_id, title: fakeFileName}]; + expect(spyCompleteFileUploadToSlack.calledOnceWithExactly(mockedSlackClient, expectedFiles, fakeChannelId, fakeThreadTs)).toBeTruthy(); + + sinon.restore() + }); + +}); + +describe('uploadAllScreenshotsToSlackThread', () => { + /** + * Given a Slack client, files array, channel, and threadTs + * When `uploadAllScreenshotsToSlackThread()` is called + * Then the call is delegated to `uploadScreenshotToSlackThread()` + */ + test('Uploads all screenshots to a Slack thread', async () => { + // Given + const mockedSlackClient = sinon.stub(); + const spyUploadScreenshotToSlackThread = sinon.stub() + const fakeDownloadResponses = [ + {content: {Body: 'fake-body-1'}, key: 'fake-key-2'}, + {content: {Body: 'fake-body-1'}, key: 'fake-key-2'}, + ] + const overriddenDependencies = { + uploadScreenshotToSlackThread: spyUploadScreenshotToSlackThread, + }; + const fakeChannelID = 'fake-channel-id'; + const fakeThreadTsValue = 'fake-thread-ts-value'; + + // When + await index.uploadAllScreenshotsToSlackThread( + fakeDownloadResponses, + mockedSlackClient, + fakeChannelID, + fakeThreadTsValue, + overriddenDependencies + ); + + // Then + expect(spyUploadScreenshotToSlackThread.calledTwice).toBeTruthy() + const expectedArgs = [ + [fakeDownloadResponses[0], mockedSlackClient, fakeChannelID, fakeThreadTsValue], + [fakeDownloadResponses[1], mockedSlackClient, fakeChannelID, fakeThreadTsValue], + ] + expect(spyUploadScreenshotToSlackThread.args).toStrictEqual(expectedArgs) + + }); +}); + +describe('sendSlackPost', () => { + /** + * Given a Slack client, payload, and channelId + * When `sendSlackPost()` is called + * Then it should send a post request to the Slack API + */ + test('Sends a post request to the Slack API', async () => { + // Given + const mockedSlackClient = { + chat: { + postMessage: sinon.stub(), + }, token: 'fake-token', + }; + const fakeChannelId = 'C123' + const payload = {text: 'Test', channel: fakeChannelId}; + + // When + const result = await index.sendSlackPost(mockedSlackClient, payload, fakeChannelId); + + // Then + sinon.assert.calledOnceWithExactly(mockedSlackClient.chat.postMessage, { + token: mockedSlackClient.token, + channel: fakeChannelId, + blocks: payload, + text: 'Synthetic monitoring alert raised' + }); + }); +}); + +describe('extractReport', () => { + /** + * Given a report key and an S3 client + * When `extractReport()` is called + * Then it should return the report content + */ + test('Extracts the report content from S3', async () => { + // Given + const fakeS3BucketName = 'fake-s3-bucket-name-value'; + const failedScreenshotKey = 'canary/eu-west-2/test-env-display/2024/08/08/15-02-01/01-covid-19-failed.png' + const BrokenKeyLinksReportKey = 'canary/eu-west-2/test-env-display/2024/08/08/15-02-01/BrokenLinkCheckerReport.json' + const folderContents = [{Key: failedScreenshotKey}, {Key: BrokenKeyLinksReportKey},]; + const keyToSearchFor = 'BrokenLinkCheckerReport' + + const spyExtractReportKey = sinon.stub().returns(BrokenKeyLinksReportKey); + const spyDownloadFile = sinon.stub().returns({Body: 'fake-stream'}); + const spyStreamToBuffer = sinon.stub().resolves(Buffer.from('{}', 'utf8')); + const mockedS3Client = sinon.stub() + const overriddenDependencies = { + extractReportKey: spyExtractReportKey, + downloadFile: spyDownloadFile, + streamToBuffer: spyStreamToBuffer, + }; + + // When + const result = await index.extractReport(folderContents, keyToSearchFor, fakeS3BucketName, mockedS3Client, overriddenDependencies); + + // Then + expect(spyExtractReportKey.calledOnceWithExactly(folderContents, keyToSearchFor)).toBeTruthy(); + expect(spyDownloadFile.calledOnceWithExactly(fakeS3BucketName, BrokenKeyLinksReportKey, mockedS3Client)).toBeTruthy(); + expect(spyStreamToBuffer.calledOnceWithExactly('fake-stream')).toBeTruthy(); + + sinon.restore() + }); +}); + +describe('extractTargetFromEvent', () => { + /** + * Given an event object + * When `extractTargetFromEvent()` is called + * Then it should return the target property from the event + */ + test('Extracts the target property from the event', () => { + // Given + const fakeTarget = 'this-is-the-target' + const fakeEvent = { + 'Records': [{ + 'Sns': { + 'Message': JSON.stringify({ + Trigger: {Dimensions: [{value: fakeTarget}]} + }) + } + }] + } + + // When + const result = index.extractTargetFromEvent(fakeEvent); + + // Then + expect(result).toBe(fakeTarget); + }); +}); + +describe('determineRelevantFolderInS3', () => { + /** + * Given an S3 client and prefix + * When `determineRelevantFolderInS3()` is called + * Then it should return the most relevant folder in S3 + */ + test('Determines the relevant folder in S3 based on prefix', async () => { + // Given + const fakeTarget = 'this-is-the-target' + const fakeEvent = { + 'Records': [{ + 'Sns': { + 'Message': JSON.stringify({ + Trigger: {Dimensions: [{value: fakeTarget}]} + }) + } + }] + } + const expectedPrefix = "abc/xyz" + const spyGetRelevantPrefix = sinon.stub().returns(expectedPrefix) + const injectedDependencies = { + getRelevantPrefix: spyGetRelevantPrefix + } + + // When + const result = await index.determineRelevantFolderInS3(fakeEvent, injectedDependencies); + + // Then + expect(spyGetRelevantPrefix.calledOnceWithExactly(fakeTarget)) + expect(result).toStrictEqual(expectedPrefix); + }); +}); + +describe('handler', () => { + let spyDetermineRelevantFolderInS3; + let spyGetSlackSecret; + let spyBuildSlackClient; + let spyListFiles; + let spyExtractReport; + let spyBuildSlackPostPayload; + let spySendSlackPost; + let spyExtractFailedScreenshotKeys; + let spyDownloadAllFiles; + let spyUploadAllScreenshotsToSlackThread; + let injectedDependencies; + + const event = {someKey: 'someValue'}; + const slackSecret = {slack_token: 'test-token', slack_channel_id: 'channel-id'}; + const slackClient = {someClientProperty: 'value'}; + const listedFiles = {Contents: ['file1', 'file2']}; + const syntheticsReport = {canaryName: 'Test Canary', startTime: 'start-time', endTime: 'end-time'}; + const brokenLinksReport = {brokenLinks: ['link1', 'link2']}; + const slackPayload = {text: 'payload'}; + const slackPostResponse = {ts: 'timestamp'}; + const extractedSnapshotKeys = ['key1', 'key2']; + const downloadResponses = ['response1', 'response2']; + + beforeEach(() => { + spyDetermineRelevantFolderInS3 = sinon.stub().resolves('relevant-folder'); + spyGetSlackSecret = sinon.stub().resolves(slackSecret); + spyBuildSlackClient = sinon.stub().resolves(slackClient); + spyListFiles = sinon.stub().resolves(listedFiles); + spyExtractReport = sinon.stub(); + spyExtractReport.withArgs(listedFiles.Contents, 'SyntheticsReport').resolves(syntheticsReport); + spyExtractReport.withArgs(listedFiles.Contents, 'BrokenLinkCheckerReport').resolves(brokenLinksReport); + spyBuildSlackPostPayload = sinon.stub().returns(slackPayload); + spySendSlackPost = sinon.stub().resolves(slackPostResponse); + spyExtractFailedScreenshotKeys = sinon.stub().returns(extractedSnapshotKeys); + spyDownloadAllFiles = sinon.stub().resolves(downloadResponses); + spyUploadAllScreenshotsToSlackThread = sinon.stub().resolves(); + + injectedDependencies = { + determineRelevantFolderInS3: spyDetermineRelevantFolderInS3, + getSlackSecret: spyGetSlackSecret, + buildSlackClient: spyBuildSlackClient, + listFiles: spyListFiles, + extractReport: spyExtractReport, + buildSlackPostPayload: spyBuildSlackPostPayload, + sendSlackPost: spySendSlackPost, + extractFailedScreenshotKeys: spyExtractFailedScreenshotKeys, + downloadAllFiles: spyDownloadAllFiles, + uploadAllScreenshotsToSlackThread: spyUploadAllScreenshotsToSlackThread + }; + }); + + afterEach(() => { + sinon.restore(); + }); + + /** + * Given an event object and an S3 bucket name + * When `handler()` is called + * Then a `WebClient` is created with the correct secret token + */ + test('should create a `WebClient` for Slack with the token pulled from `SecretsManager`', async () => { + // Given + const fakeSlackToken = 'some-fake-slack-auth-token'; + spyGetSlackSecret.returns({slack_token: fakeSlackToken}); + + // When + await index.handler(event, sinon.stub(), injectedDependencies); + + // Then + expect(spyGetSlackSecret.calledOnce).toBeTruthy() + expect(spyBuildSlackClient.calledOnceWithExactly(fakeSlackToken)).toBeTruthy() + }); + + /** + * Given an event object and an S3 bucket name + * When `handler()` is called + * Then the target folder in the S3 bucket is found and listed + */ + test('should extract the relevant folder contents', async () => { + // Given + const relevantFolder = 'abc/relevant-folder/' + const fakeBucketName = 'fake-bucket-name-value' + const mockedEvent = sinon.stub() + spyDetermineRelevantFolderInS3.returns(relevantFolder) + + // When + await index.handler(mockedEvent, fakeBucketName, injectedDependencies); + + // Then + expect(spyDetermineRelevantFolderInS3.calledOnceWithExactly(mockedEvent)).toBeTruthy() + expect(spyListFiles.calledOnceWithExactly(fakeBucketName, relevantFolder)).toBeTruthy() + }); + + /** + * Given an event object and an S3 bucket name + * When `handler()` is called + * Then the reports and failed screenshots + * are pulled from the target folder in the S3 bucket + */ + test('should extract the relevant reports and failed screenshots', async () => { + // Given + const failedScreenshotKey = 'canary/eu-west-2/test-env-display/2024/08/08/15-02-01/01-covid-19-failed.png' + const BrokenKeyLinksReportKey = 'canary/eu-west-2/test-env-display/2024/08/08/15-02-01/BrokenLinkCheckerReport.json' + const folderContents = [{Key: failedScreenshotKey}, {Key: BrokenKeyLinksReportKey},]; + spyListFiles.returns({Contents: folderContents}) + spyExtractReport.returns({canaryName: "abc", startTime: "def", endTime: "xyz", brokenLinks: []}) + + // When + await index.handler(sinon.stub(), sinon.stub(), injectedDependencies); + + // Then + expect(spyExtractReport.calledTwice).toBeTruthy() + const expectedArgs = [ + [folderContents, 'SyntheticsReport'], + [folderContents, 'BrokenLinkCheckerReport'], + ] + expect(spyExtractReport.args).toStrictEqual(expectedArgs) + }); + + /** + * Given an event object and an S3 bucket name + * When `handler()` is called + * Then a post is sent to the Slack channel + */ + test('should post the primary message to Slack', async () => { + // Given + const infoExtractedFromReports = {canaryName: "abc", startTime: "def", endTime: "xyz", brokenLinks: []} + spyExtractReport.returns(infoExtractedFromReports) + const fakeSlackPayload = {blocks: []} + spyBuildSlackPostPayload.returns(fakeSlackPayload) + const fakeSlackChannelId = 'some-fake-slack-channel-id'; + spyGetSlackSecret.returns({slack_channel_id: fakeSlackChannelId}); + + // When + await index.handler(sinon.stub(), sinon.stub(), injectedDependencies); + + // Then + expect(spyBuildSlackPostPayload.calledOnceWithExactly(infoExtractedFromReports)) + expect(spySendSlackPost.calledOnceWithExactly(slackClient, fakeSlackPayload, fakeSlackChannelId)) + }); + + /** + * Given an event object and an S3 bucket name + * When `handler()` is called + * Then the failed screenshots are uploaded + * to the primary post which was sent to the Slack channel + */ + test('should get the failed screenshots and upload them to Slack', async () => { + // Given + const mockedFolderContents = sinon.stub() + const bucketName = 'fake-bucket-name-value' + spyListFiles.returns({Contents: mockedFolderContents}) + const extractedSnapshotKeys = ["abc-failed.png", "xyz-failed.png"] + const infoExtractedFromReports = {canaryName: "abc", startTime: "def", endTime: "xyz", brokenLinks: []} + spyExtractReport.returns(infoExtractedFromReports) + + const fakeSlackChannelId = 'some-fake-slack-channel-id'; + spyGetSlackSecret.returns({slack_channel_id: fakeSlackChannelId}); + + const slackPostResponse = { + 'ts': 'abc' + } + spySendSlackPost.returns(slackPostResponse) + + // When + await index.handler(sinon.stub(), bucketName, injectedDependencies); + + // Then + expect(spyExtractFailedScreenshotKeys.calledOnceWithExactly(mockedFolderContents)) + expect(spyDownloadAllFiles.calledOnceWithExactly(extractedSnapshotKeys, bucketName)) + expect(spyUploadAllScreenshotsToSlackThread.calledOnceWithExactly( + spyDownloadAllFiles.result, + spyBuildSlackClient.result, + fakeSlackChannelId, + slackPostResponse.ts + )) + + }); + +}); \ No newline at end of file From 3c9c36d73fd28e70b55b5fa70cdb6239c6c0b725 Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Fri, 9 Aug 2024 14:29:14 +0100 Subject: [PATCH 40/59] Remove unused functions from being exported --- src/lambda-canary-notification/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/lambda-canary-notification/index.js b/src/lambda-canary-notification/index.js index 2cb1edef..65a4dec4 100644 --- a/src/lambda-canary-notification/index.js +++ b/src/lambda-canary-notification/index.js @@ -563,7 +563,6 @@ module.exports = { streamToBuffer, downloadFile, downloadAllFiles, - buildSlackClient, getFileUploadURLToSlack, sendPostRequest, completeFileUploadToSlack, @@ -573,7 +572,6 @@ module.exports = { extractFailedScreenshotKeys, extractReportKey, buildSlackPostPayload, - buildBrokenLinksList, uploadScreenshotToSlackThread, uploadAllScreenshotsToSlackThread, sendSlackPost, From cf9a3f33feaa4d61ec910f6fd0588ff32c95f335 Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Fri, 9 Aug 2024 15:13:29 +0100 Subject: [PATCH 41/59] Revoke security group rules for cloudwatch canary module security group on delete --- terraform/modules/cloud-watch-canary/security-group.tf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/terraform/modules/cloud-watch-canary/security-group.tf b/terraform/modules/cloud-watch-canary/security-group.tf index c7ec427d..71ee4acb 100644 --- a/terraform/modules/cloud-watch-canary/security-group.tf +++ b/terraform/modules/cloud-watch-canary/security-group.tf @@ -5,6 +5,8 @@ module "canary_security_group" { name = var.name vpc_id = var.vpc_id + revoke_rules_on_delete = true + egress_with_cidr_blocks = [ { description = "https to internet" From 4b381507378eb94a7e1aa82bbfb64ab03b422aeb Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Mon, 12 Aug 2024 14:25:56 +0100 Subject: [PATCH 42/59] Provide lambda context to `handler()` --- src/lambda-canary-notification/index.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/lambda-canary-notification/index.js b/src/lambda-canary-notification/index.js index 65a4dec4..a06c1f2a 100644 --- a/src/lambda-canary-notification/index.js +++ b/src/lambda-canary-notification/index.js @@ -509,12 +509,11 @@ async function determineRelevantFolderInS3(event, overriddenDependencies = {}) { * Main handler entrypoint for the Lambda runtime execution. * * @param {object} event - The object passed down to the Lambda runtime on initialization. - * @param {string} bucketName - The name of the S3 bucket to search in. - * Defaults to the env var `S3_CANARY_LOGS_BUCKET_NAME` + * @param {Object} context - The Lambda execution context. * @param overriddenDependencies - Object used to override the default dependencies. * */ -async function handler(event, bucketName = S3_CANARY_LOGS_BUCKET_NAME, overriddenDependencies = {}) { +async function handler(event, context, overriddenDependencies = {}) { const defaultDependencies = { determineRelevantFolderInS3, getSlackSecret, From c71580c1c796f6e02d78aadc385914cd293cec9c Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Mon, 12 Aug 2024 14:26:28 +0100 Subject: [PATCH 43/59] Pass `S3_CANARY_LOGS_BUCKET_NAME` downstream into callee functions --- src/lambda-canary-notification/index.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/lambda-canary-notification/index.js b/src/lambda-canary-notification/index.js index a06c1f2a..8ce7e7e2 100644 --- a/src/lambda-canary-notification/index.js +++ b/src/lambda-canary-notification/index.js @@ -11,8 +11,6 @@ const axios = require('axios'); const FormData = require('form-data'); const path = require('path'); -const S3_CANARY_LOGS_BUCKET_NAME = process.env.S3_CANARY_LOGS_BUCKET_NAME; - /** * Gets the filename associated with the s3 key / file path * @@ -212,7 +210,7 @@ async function getRelevantPrefix(target, s3Client = new S3Client()) { const {year, month, day, hour} = getCurrentDate() const prefix = `canary/eu-west-2/${target}/${year}/${month}/${day}/${hour}` try { - const data = await listFiles(S3_CANARY_LOGS_BUCKET_NAME, prefix, s3Client) + const data = await listFiles(process.env.S3_CANARY_LOGS_BUCKET_NAME, prefix, s3Client) const folders = new Set(data.Contents.map(item => { const parts = item.Key.split('/'); @@ -446,7 +444,6 @@ async function sendSlackPost(slackClient, payload, channelId) { * @param {array} folderContents - Array of objects representing the listed folder contents * @param {string} keyToSearchFor - The key to filter for in the given `keys` * @param {string} bucketName - The name of the S3 bucket to search in. - * Defaults to the env var `S3_CANARY_LOGS_BUCKET_NAME` * @param {S3Client} s3Client - An optional instance of the S3Client to use for downloading the file. * @param overriddenDependencies - Object used to override the default dependencies. * @@ -455,7 +452,7 @@ async function sendSlackPost(slackClient, payload, channelId) { async function extractReport( folderContents, keyToSearchFor, - bucketName = S3_CANARY_LOGS_BUCKET_NAME, + bucketName, s3Client = new S3Client(), overriddenDependencies = {} ) { @@ -527,6 +524,7 @@ async function handler(event, context, overriddenDependencies = {}) { uploadAllScreenshotsToSlackThread }; const dependencies = {...defaultDependencies, ...overriddenDependencies} + const bucketName = process.env.S3_CANARY_LOGS_BUCKET_NAME const slackSecret = await dependencies.getSlackSecret() const slackClient = await dependencies.buildSlackClient(slackSecret.slack_token) @@ -536,8 +534,8 @@ async function handler(event, context, overriddenDependencies = {}) { const listedFiles = await dependencies.listFiles(bucketName, relevantFolder) const folderContents = listedFiles.Contents - const syntheticsReport = await dependencies.extractReport(folderContents, 'SyntheticsReport') - const brokenLinksReport = await dependencies.extractReport(folderContents, 'BrokenLinkCheckerReport') + const syntheticsReport = await dependencies.extractReport(folderContents, 'SyntheticsReport', bucketName) + const brokenLinksReport = await dependencies.extractReport(folderContents, 'BrokenLinkCheckerReport', bucketName) const slackPayload = dependencies.buildSlackPostPayload( syntheticsReport.canaryName, From e09d7ebaf28123f247fe4ceb4fa9d22ff9737c51 Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Mon, 12 Aug 2024 14:26:49 +0100 Subject: [PATCH 44/59] Update tests to reflect capturing of env var for `S3_CANARY_LOGS_BUCKET_NAME` --- src/lambda-canary-notification/index.test.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/lambda-canary-notification/index.test.js b/src/lambda-canary-notification/index.test.js index 5dcf6df2..b57befd0 100644 --- a/src/lambda-canary-notification/index.test.js +++ b/src/lambda-canary-notification/index.test.js @@ -722,15 +722,17 @@ describe('handler', () => { // Given const relevantFolder = 'abc/relevant-folder/' const fakeBucketName = 'fake-bucket-name-value' + const mockedEnvVar = sinon.stub(process, 'env').value({S3_CANARY_LOGS_BUCKET_NAME: fakeBucketName}); const mockedEvent = sinon.stub() spyDetermineRelevantFolderInS3.returns(relevantFolder) // When - await index.handler(mockedEvent, fakeBucketName, injectedDependencies); + await index.handler(mockedEvent, sinon.stub(), injectedDependencies); // Then expect(spyDetermineRelevantFolderInS3.calledOnceWithExactly(mockedEvent)).toBeTruthy() expect(spyListFiles.calledOnceWithExactly(fakeBucketName, relevantFolder)).toBeTruthy() + mockedEnvVar.restore(); }); /** @@ -746,6 +748,8 @@ describe('handler', () => { const folderContents = [{Key: failedScreenshotKey}, {Key: BrokenKeyLinksReportKey},]; spyListFiles.returns({Contents: folderContents}) spyExtractReport.returns({canaryName: "abc", startTime: "def", endTime: "xyz", brokenLinks: []}) + const fakeBucketName = 'fake-bucket-name-value' + const mockedEnvVar = sinon.stub(process, 'env').value({S3_CANARY_LOGS_BUCKET_NAME: fakeBucketName}); // When await index.handler(sinon.stub(), sinon.stub(), injectedDependencies); @@ -753,10 +757,11 @@ describe('handler', () => { // Then expect(spyExtractReport.calledTwice).toBeTruthy() const expectedArgs = [ - [folderContents, 'SyntheticsReport'], - [folderContents, 'BrokenLinkCheckerReport'], + [folderContents, 'SyntheticsReport', fakeBucketName], + [folderContents, 'BrokenLinkCheckerReport', fakeBucketName], ] expect(spyExtractReport.args).toStrictEqual(expectedArgs) + mockedEnvVar.restore(); }); /** From 2bed52e9320b15f7a1445a0116340b704003db92 Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Mon, 12 Aug 2024 16:35:05 +0100 Subject: [PATCH 45/59] Extract script from `src/` directory, output to ignored `builds/` directory --- terraform/20-app/cloudwatch.canary.front-end.tf | 8 ++++---- terraform/modules/cloud-watch-canary/script.tf | 4 ++-- terraform/modules/cloud-watch-canary/vars.tf | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/terraform/20-app/cloudwatch.canary.front-end.tf b/terraform/20-app/cloudwatch.canary.front-end.tf index 3027c2a2..0b7e16fb 100644 --- a/terraform/20-app/cloudwatch.canary.front-end.tf +++ b/terraform/20-app/cloudwatch.canary.front-end.tf @@ -11,10 +11,10 @@ module "cloudwatch_canary_front_end_screenshots" { vpc_id = module.vpc.vpc_id subnet_ids = module.vpc.private_subnets - schedule_expression = "rate(10 minutes)" - timeout_in_seconds = 600 - script_path = "../../src/canary-front-end-broken-links" - lambda_function_notification_arn = module.lambda_canary_notification.lambda_function_arn + schedule_expression = "rate(10 minutes)" + timeout_in_seconds = 600 + src_script_path = "canary-front-end-broken-links" + lambda_function_notification_arn = module.lambda_canary_notification.lambda_function_arn environment_variables = { SITEMAP_URL = "${local.urls.front_end}/sitemap.xml" diff --git a/terraform/modules/cloud-watch-canary/script.tf b/terraform/modules/cloud-watch-canary/script.tf index 4bb661c9..0b764d21 100644 --- a/terraform/modules/cloud-watch-canary/script.tf +++ b/terraform/modules/cloud-watch-canary/script.tf @@ -1,7 +1,7 @@ locals { - script_content = file("${var.script_path}/index.js") + script_content = file("../../src/${var.src_script_path}/index.js") script_content_hash = sha256(local.script_content) - zip = "builds/${var.script_path}-${local.script_content_hash}.zip" + zip = "builds/${var.src_script_path}-${local.script_content_hash}.zip" } data "archive_file" "canary_script" { diff --git a/terraform/modules/cloud-watch-canary/vars.tf b/terraform/modules/cloud-watch-canary/vars.tf index 6f9aa299..ac36bd71 100644 --- a/terraform/modules/cloud-watch-canary/vars.tf +++ b/terraform/modules/cloud-watch-canary/vars.tf @@ -39,8 +39,8 @@ variable "timeout_in_seconds" { type = number } -variable "script_path" { - description = "The file path of the script to attach to the canary" +variable "src_script_path" { + description = "The src file path of the script to attach to the canary" type = string } From e1b571c456a8aff6d9bdf06142d01e2990df53ff Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Wed, 14 Aug 2024 16:35:16 +0100 Subject: [PATCH 46/59] Set analysed period of alarm to be `timeout_in_seconds` variable --- terraform/modules/cloud-watch-canary/cloudwatch.alarm.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terraform/modules/cloud-watch-canary/cloudwatch.alarm.tf b/terraform/modules/cloud-watch-canary/cloudwatch.alarm.tf index 5ad6ba66..49fa0a07 100644 --- a/terraform/modules/cloud-watch-canary/cloudwatch.alarm.tf +++ b/terraform/modules/cloud-watch-canary/cloudwatch.alarm.tf @@ -13,7 +13,7 @@ module "cloudwatch_alarm" { comparison_operator = "GreaterThanOrEqualToThreshold" evaluation_periods = 1 threshold = 1 - period = 60 + period = var.timeout_in_seconds dimensions = { CanaryName = aws_synthetics_canary.this.name } From 0817d3f441d2079fd9b06e47770a997f0e387692 Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Fri, 16 Aug 2024 11:49:03 +0100 Subject: [PATCH 47/59] Set alarm to trigger when `SuccessPercent` dips below 100% --- terraform/modules/cloud-watch-canary/cloudwatch.alarm.tf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/terraform/modules/cloud-watch-canary/cloudwatch.alarm.tf b/terraform/modules/cloud-watch-canary/cloudwatch.alarm.tf index 49fa0a07..70759dc4 100644 --- a/terraform/modules/cloud-watch-canary/cloudwatch.alarm.tf +++ b/terraform/modules/cloud-watch-canary/cloudwatch.alarm.tf @@ -10,14 +10,14 @@ module "cloudwatch_alarm" { } ) alarm_actions = [module.sns_topic_alarm.topic_arn] - comparison_operator = "GreaterThanOrEqualToThreshold" + comparison_operator = "LessThanOrEqualToThreshold" evaluation_periods = 1 - threshold = 1 + threshold = 99 period = var.timeout_in_seconds dimensions = { CanaryName = aws_synthetics_canary.this.name } namespace = "CloudWatchSynthetics" - metric_name = "Failed requests" + metric_name = "SuccessPercent" statistic = "Sum" } From f8dae0b89f1e567f8c8d545134a36222609e835c Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Mon, 19 Aug 2024 10:44:22 +0100 Subject: [PATCH 48/59] Set notification lambda to consume event from eventbridge --- src/lambda-canary-notification/index.js | 230 ++++-------------- src/lambda-canary-notification/index.test.js | 125 ++-------- .../20-app/cloudwatch.canary.front-end.tf | 1 + .../20-app/lambda.canary-notification.tf | 7 +- .../cloud-watch-canary/cloudwatch.alarm.tf | 23 -- .../modules/cloud-watch-canary/eventbridge.tf | 23 ++ .../modules/cloud-watch-canary/outputs.tf | 10 +- .../modules/cloud-watch-canary/sns.alarm.tf | 11 - terraform/modules/cloud-watch-canary/vars.tf | 5 + 9 files changed, 111 insertions(+), 324 deletions(-) delete mode 100644 terraform/modules/cloud-watch-canary/cloudwatch.alarm.tf create mode 100644 terraform/modules/cloud-watch-canary/eventbridge.tf delete mode 100644 terraform/modules/cloud-watch-canary/sns.alarm.tf diff --git a/src/lambda-canary-notification/index.js b/src/lambda-canary-notification/index.js index 8ce7e7e2..165ff2d4 100644 --- a/src/lambda-canary-notification/index.js +++ b/src/lambda-canary-notification/index.js @@ -1,9 +1,5 @@ const { - S3Client, - GetObjectCommand, - ListObjectsV2Command, - ListObjectsV2CommandOutput, - GetObjectCommandOutput + S3Client, GetObjectCommand, ListObjectsV2Command, ListObjectsV2CommandOutput, GetObjectCommandOutput } = require("@aws-sdk/client-s3"); const {SecretsManagerClient, GetSecretValueCommand} = require("@aws-sdk/client-secrets-manager"); const {WebClient} = require("@slack/web-api") @@ -183,6 +179,12 @@ function getDirectoryPath(filePath) { return parts.join('/'); } +function getPathFromArtifactLocation(artifactLocation) { + const index = artifactLocation.indexOf('/'); + return artifactLocation.substring(index + 1); +} + + /** * Gets the current datetime containing the year, month, day and hour. The month, day and hours are padded to 2 digits. * @@ -199,34 +201,6 @@ function getCurrentDate() { return {year, month, day, hour} } -/** - * Gets the target folder/prefix in the s3 bucket for the required files - * - * @param {string} target - The mid-prefix / canary name to filter against. - * @param {S3Client} s3Client - An optional instance of the S3Client to use for sending the command. - * @returns {string} - The relevant prefix for the required files in the s3 bucket - */ -async function getRelevantPrefix(target, s3Client = new S3Client()) { - const {year, month, day, hour} = getCurrentDate() - const prefix = `canary/eu-west-2/${target}/${year}/${month}/${day}/${hour}` - try { - const data = await listFiles(process.env.S3_CANARY_LOGS_BUCKET_NAME, prefix, s3Client) - - const folders = new Set(data.Contents.map(item => { - const parts = item.Key.split('/'); - return parts.slice(0, 9).join('/'); - })); - - const folderArray = Array.from(folders); - - folderArray.sort((a, b) => (a < b ? 1 : -1)); - return getDirectoryPath(folderArray[0]) - } catch (error) { - console.error('Error fetching the latest folder:', error); - throw error; - } -} - /** * Extracts the keys associated with the failed page snapshots from the given keys * @@ -265,36 +239,21 @@ function extractReportKey(keys, keyToSearchFor) { * @returns {array} - An array of JSON objects which can be used to post to the Slack channel with. */ function buildSlackPostPayload(target, startTime, endTime, brokenLinks) { - return [ - { - "type": "header", - "text": { - "type": "plain_text", - "text": ":alert: Canary run failed", - "emoji": true - } - }, - { - "type": "divider" - }, - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": `*Alarm name:*\n${target}` - } - }, - buildBrokenLinksList(brokenLinks), - { - "type": "context", - "elements": [ - { - "type": "plain_text", - "text": `Canary started at ${startTime} and failed at ${endTime}` - } - ] - }, - ] + return [{ + "type": "header", "text": { + "type": "plain_text", "text": ":alert: Canary run failed", "emoji": true + } + }, { + "type": "divider" + }, { + "type": "section", "text": { + "type": "mrkdwn", "text": `*Alarm name:*\n${target}` + } + }, buildBrokenLinksList(brokenLinks), { + "type": "context", "elements": [{ + "type": "plain_text", "text": `Canary started at ${startTime} and failed at ${endTime}` + }] + },] } /** @@ -306,38 +265,20 @@ function buildSlackPostPayload(target, startTime, endTime, brokenLinks) { */ function buildBrokenLinksList(brokenLinks) { const blocks = { - "type": "rich_text", - "elements": [ - { - "type": "rich_text_section", - "elements": [ - { - "type": "text", - "text": "Detected broken link(s):\n" - }, - ] - }, - { - "type": "rich_text_list", - "style": "bullet", - "indent": 0, - "border": 0, - "elements": [] - } - ] + "type": "rich_text", "elements": [{ + "type": "rich_text_section", "elements": [{ + "type": "text", "text": "Detected broken link(s):\n" + },] + }, { + "type": "rich_text_list", "style": "bullet", "indent": 0, "border": 0, "elements": [] + }] } brokenLinks.forEach(brokenLink => { - blocks.elements[1].elements.push( - { - "type": "rich_text_section", - "elements": [ - { - "type": "link", - "url": brokenLink - } - ] - }, - ); + blocks.elements[1].elements.push({ + "type": "rich_text_section", "elements": [{ + "type": "link", "url": brokenLink + }] + },); }); return blocks @@ -354,46 +295,23 @@ function buildBrokenLinksList(brokenLinks) { * * @returns {Promise} - The promise from the function call */ -async function uploadScreenshotToSlackThread( - downloadedFileResponse, - slackClient, - channelId, - threadTs, - overriddenDependencies = {} -) { +async function uploadScreenshotToSlackThread(downloadedFileResponse, slackClient, channelId, threadTs, overriddenDependencies = {}) { const defaultDependencies = { - streamToBuffer, - getFilename, - getFileUploadURLToSlack, - sendPostRequest, - completeFileUploadToSlack + streamToBuffer, getFilename, getFileUploadURLToSlack, sendPostRequest, completeFileUploadToSlack }; const dependencies = {...defaultDependencies, ...overriddenDependencies} const fileBufferStream = await dependencies.streamToBuffer(downloadedFileResponse.content.Body); const fileName = dependencies.getFilename(downloadedFileResponse.key); - const fileUploadURLResponse = await dependencies.getFileUploadURLToSlack( - slackClient, - fileName, - fileBufferStream.length - ) + const fileUploadURLResponse = await dependencies.getFileUploadURLToSlack(slackClient, fileName, fileBufferStream.length) const uploadURL = fileUploadURLResponse.upload_url const fileID = fileUploadURLResponse.file_id - await dependencies.sendPostRequest( - uploadURL, - fileName, - fileBufferStream - ) + await dependencies.sendPostRequest(uploadURL, fileName, fileBufferStream) const files = [{"id": fileID, "title": fileName}] - await dependencies.completeFileUploadToSlack( - slackClient, - files, - channelId, - threadTs - ) + await dependencies.completeFileUploadToSlack(slackClient, files, channelId, threadTs) } /** @@ -407,13 +325,7 @@ async function uploadScreenshotToSlackThread( * * @returns {Promise} - The promise from the function call */ -async function uploadAllScreenshotsToSlackThread( - downloadedFileResponses, - slackClient, - channelId, - threadTs, - overriddenDependencies = {} -) { +async function uploadAllScreenshotsToSlackThread(downloadedFileResponses, slackClient, channelId, threadTs, overriddenDependencies = {}) { const defaultDependencies = { uploadScreenshotToSlackThread, }; @@ -449,17 +361,9 @@ async function sendSlackPost(slackClient, payload, channelId) { * * @returns {object} - The JSON representation of the contents of the report file. */ -async function extractReport( - folderContents, - keyToSearchFor, - bucketName, - s3Client = new S3Client(), - overriddenDependencies = {} -) { +async function extractReport(folderContents, keyToSearchFor, bucketName, s3Client = new S3Client(), overriddenDependencies = {}) { const defaultDependencies = { - extractReportKey, - downloadFile, - streamToBuffer, + extractReportKey, downloadFile, streamToBuffer, }; const dependencies = {...defaultDependencies, ...overriddenDependencies} @@ -472,36 +376,6 @@ async function extractReport( return JSON.parse(jsonString); } -/** - * Extracts the name of the triggered Canary from the `event` object passed to the Lambda runtime. - * - * @param {object} event - The object passed down to the Lambda runtime on initialization. - * @returns {string} - The name of the Canary being triggered. - */ -function extractTargetFromEvent(event) { - const eventMessage = JSON.parse(event.Records[0].Sns.Message); - return eventMessage.Trigger.Dimensions[0].value -} - -/** - * Calculates the relevant folder in s3 relating to the triggered Canary results. - * - * @param {object} event - The object passed down to the Lambda runtime on initialization. - * @param overriddenDependencies - Object used to override the default dependencies. - * - * @returns {string} - The prefix associated with the 'folder' of the triggered Canary results. - */ -async function determineRelevantFolderInS3(event, overriddenDependencies = {}) { - const defaultDependencies = { - extractTargetFromEvent, - getRelevantPrefix, - }; - const dependencies = {...defaultDependencies, ...overriddenDependencies} - - const target = dependencies.extractTargetFromEvent(event) - return await dependencies.getRelevantPrefix(target) -} - /** * Main handler entrypoint for the Lambda runtime execution. * @@ -512,7 +386,6 @@ async function determineRelevantFolderInS3(event, overriddenDependencies = {}) { */ async function handler(event, context, overriddenDependencies = {}) { const defaultDependencies = { - determineRelevantFolderInS3, getSlackSecret, buildSlackClient, listFiles, @@ -526,23 +399,23 @@ async function handler(event, context, overriddenDependencies = {}) { const dependencies = {...defaultDependencies, ...overriddenDependencies} const bucketName = process.env.S3_CANARY_LOGS_BUCKET_NAME + const relevantFolder = getPathFromArtifactLocation(event.detail["artifact-location"]) + const runStatus = event.detail["test-run-status"] + if (runStatus === "PASSED") { + console.log('Canary run passed') + return + } + const slackSecret = await dependencies.getSlackSecret() const slackClient = await dependencies.buildSlackClient(slackSecret.slack_token) - const relevantFolder = await dependencies.determineRelevantFolderInS3(event) - const listedFiles = await dependencies.listFiles(bucketName, relevantFolder) const folderContents = listedFiles.Contents const syntheticsReport = await dependencies.extractReport(folderContents, 'SyntheticsReport', bucketName) const brokenLinksReport = await dependencies.extractReport(folderContents, 'BrokenLinkCheckerReport', bucketName) - const slackPayload = dependencies.buildSlackPostPayload( - syntheticsReport.canaryName, - syntheticsReport.startTime, - syntheticsReport.endTime, - brokenLinksReport.brokenLinks - ) + const slackPayload = dependencies.buildSlackPostPayload(syntheticsReport.canaryName, syntheticsReport.startTime, syntheticsReport.endTime, brokenLinksReport.brokenLinks) const slackPostResponse = await dependencies.sendSlackPost(slackClient, slackPayload, slackSecret.slack_channel_id) const extractedSnapshotKeys = dependencies.extractFailedScreenshotKeys(folderContents) @@ -565,7 +438,6 @@ module.exports = { completeFileUploadToSlack, getDirectoryPath, getCurrentDate, - getRelevantPrefix, extractFailedScreenshotKeys, extractReportKey, buildSlackPostPayload, @@ -573,6 +445,4 @@ module.exports = { uploadAllScreenshotsToSlackThread, sendSlackPost, extractReport, - extractTargetFromEvent, - determineRelevantFolderInS3, } \ No newline at end of file diff --git a/src/lambda-canary-notification/index.test.js b/src/lambda-canary-notification/index.test.js index b57befd0..5b3ea5fa 100644 --- a/src/lambda-canary-notification/index.test.js +++ b/src/lambda-canary-notification/index.test.js @@ -300,36 +300,6 @@ describe('getCurrentDate', () => { }); }); -describe('getRelevantPrefix', () => { - /** - * Given a failed screenshot keys array and prefix - * When `getRelevantPrefix()` is called - * Then it should return the relevant prefix - */ - test('Returns the relevant prefix based on failed screenshot keys', async () => { - // Given - const frozenDate = new Date('2023-12-25T00:00:00Z'); - jest.spyOn(global, 'Date').mockImplementation(() => frozenDate); - const fakeTarget = 'uhd-fake-target'; - const fakeDataFromS3 = { - Contents: [ - {Key: `canary/eu-west-2/${fakeTarget}/2023/12/07/08/item.png`}, - {Key: `canary/eu-west-2/${fakeTarget}/2023/12/19/14/report.json`}, - {Key: `canary/eu-west-2/${fakeTarget}/2023/12/25/00/another-item.png`}, - ] - } - const mockedS3Client = { - send: sinon.stub().resolves(fakeDataFromS3), - }; - - // When - const result = await index.getRelevantPrefix(fakeTarget, mockedS3Client); - - // Then - expect(result).toStrictEqual(`canary/eu-west-2/${fakeTarget}/2023/12/25/00`); - }); -}); - describe('extractFailedScreenshotKeys', () => { /** * Given an S3 file list @@ -579,68 +549,7 @@ describe('extractReport', () => { }); }); -describe('extractTargetFromEvent', () => { - /** - * Given an event object - * When `extractTargetFromEvent()` is called - * Then it should return the target property from the event - */ - test('Extracts the target property from the event', () => { - // Given - const fakeTarget = 'this-is-the-target' - const fakeEvent = { - 'Records': [{ - 'Sns': { - 'Message': JSON.stringify({ - Trigger: {Dimensions: [{value: fakeTarget}]} - }) - } - }] - } - - // When - const result = index.extractTargetFromEvent(fakeEvent); - - // Then - expect(result).toBe(fakeTarget); - }); -}); - -describe('determineRelevantFolderInS3', () => { - /** - * Given an S3 client and prefix - * When `determineRelevantFolderInS3()` is called - * Then it should return the most relevant folder in S3 - */ - test('Determines the relevant folder in S3 based on prefix', async () => { - // Given - const fakeTarget = 'this-is-the-target' - const fakeEvent = { - 'Records': [{ - 'Sns': { - 'Message': JSON.stringify({ - Trigger: {Dimensions: [{value: fakeTarget}]} - }) - } - }] - } - const expectedPrefix = "abc/xyz" - const spyGetRelevantPrefix = sinon.stub().returns(expectedPrefix) - const injectedDependencies = { - getRelevantPrefix: spyGetRelevantPrefix - } - - // When - const result = await index.determineRelevantFolderInS3(fakeEvent, injectedDependencies); - - // Then - expect(spyGetRelevantPrefix.calledOnceWithExactly(fakeTarget)) - expect(result).toStrictEqual(expectedPrefix); - }); -}); - describe('handler', () => { - let spyDetermineRelevantFolderInS3; let spyGetSlackSecret; let spyBuildSlackClient; let spyListFiles; @@ -652,7 +561,24 @@ describe('handler', () => { let spyUploadAllScreenshotsToSlackThread; let injectedDependencies; - const event = {someKey: 'someValue'}; + const event = { + "detail-type": "Synthetics Canary TestRun Successful", + "source": "aws.synthetics", + "time": "2024-08-19T08:35:40Z", + "region": "eu-west-2", + "resources": [], + "detail": { + "canary-name": "uhd-fake-env-display", + "artifact-location": "uhd-fake-env-canary-logs/canary/eu-west-2/uhd-fake-env-display/2024/08/19/08/32-09-739", + "test-run-status": "PASSED", + "state-reason": "null", + "canary-run-timeline": { + "started": 1724056329.74, + "completed": 1724056539.871 + }, + "message": "Test run result is generated successfully" + } + }; const slackSecret = {slack_token: 'test-token', slack_channel_id: 'channel-id'}; const slackClient = {someClientProperty: 'value'}; const listedFiles = {Contents: ['file1', 'file2']}; @@ -664,7 +590,6 @@ describe('handler', () => { const downloadResponses = ['response1', 'response2']; beforeEach(() => { - spyDetermineRelevantFolderInS3 = sinon.stub().resolves('relevant-folder'); spyGetSlackSecret = sinon.stub().resolves(slackSecret); spyBuildSlackClient = sinon.stub().resolves(slackClient); spyListFiles = sinon.stub().resolves(listedFiles); @@ -678,7 +603,6 @@ describe('handler', () => { spyUploadAllScreenshotsToSlackThread = sinon.stub().resolves(); injectedDependencies = { - determineRelevantFolderInS3: spyDetermineRelevantFolderInS3, getSlackSecret: spyGetSlackSecret, buildSlackClient: spyBuildSlackClient, listFiles: spyListFiles, @@ -720,17 +644,14 @@ describe('handler', () => { */ test('should extract the relevant folder contents', async () => { // Given - const relevantFolder = 'abc/relevant-folder/' + const relevantFolder = 'canary/eu-west-2/uhd-fake-env-display/2024/08/19/08/32-09-739' const fakeBucketName = 'fake-bucket-name-value' const mockedEnvVar = sinon.stub(process, 'env').value({S3_CANARY_LOGS_BUCKET_NAME: fakeBucketName}); - const mockedEvent = sinon.stub() - spyDetermineRelevantFolderInS3.returns(relevantFolder) // When - await index.handler(mockedEvent, sinon.stub(), injectedDependencies); + await index.handler(event, sinon.stub(), injectedDependencies); // Then - expect(spyDetermineRelevantFolderInS3.calledOnceWithExactly(mockedEvent)).toBeTruthy() expect(spyListFiles.calledOnceWithExactly(fakeBucketName, relevantFolder)).toBeTruthy() mockedEnvVar.restore(); }); @@ -752,7 +673,7 @@ describe('handler', () => { const mockedEnvVar = sinon.stub(process, 'env').value({S3_CANARY_LOGS_BUCKET_NAME: fakeBucketName}); // When - await index.handler(sinon.stub(), sinon.stub(), injectedDependencies); + await index.handler(event, sinon.stub(), injectedDependencies); // Then expect(spyExtractReport.calledTwice).toBeTruthy() @@ -779,7 +700,7 @@ describe('handler', () => { spyGetSlackSecret.returns({slack_channel_id: fakeSlackChannelId}); // When - await index.handler(sinon.stub(), sinon.stub(), injectedDependencies); + await index.handler(event, sinon.stub(), injectedDependencies); // Then expect(spyBuildSlackPostPayload.calledOnceWithExactly(infoExtractedFromReports)) @@ -810,7 +731,7 @@ describe('handler', () => { spySendSlackPost.returns(slackPostResponse) // When - await index.handler(sinon.stub(), bucketName, injectedDependencies); + await index.handler(event, bucketName, injectedDependencies); // Then expect(spyExtractFailedScreenshotKeys.calledOnceWithExactly(mockedFolderContents)) diff --git a/terraform/20-app/cloudwatch.canary.front-end.tf b/terraform/20-app/cloudwatch.canary.front-end.tf index 0b7e16fb..7df84724 100644 --- a/terraform/20-app/cloudwatch.canary.front-end.tf +++ b/terraform/20-app/cloudwatch.canary.front-end.tf @@ -15,6 +15,7 @@ module "cloudwatch_canary_front_end_screenshots" { timeout_in_seconds = 600 src_script_path = "canary-front-end-broken-links" lambda_function_notification_arn = module.lambda_canary_notification.lambda_function_arn + lambda_function_notification_name = module.lambda_canary_notification.lambda_function_name environment_variables = { SITEMAP_URL = "${local.urls.front_end}/sitemap.xml" diff --git a/terraform/20-app/lambda.canary-notification.tf b/terraform/20-app/lambda.canary-notification.tf index d0d9d566..428c57ec 100644 --- a/terraform/20-app/lambda.canary-notification.tf +++ b/terraform/20-app/lambda.canary-notification.tf @@ -40,13 +40,14 @@ module "lambda_canary_notification" { create_current_version_allowed_triggers = false allowed_triggers = { - sns_cloudfront_alarms = { - principal = "sns.amazonaws.com" - source_arn = module.cloudwatch_canary_front_end_screenshots.sns_topic_arn + eventbridge = { + principal = "events.amazonaws.com" + source_arn = module.cloudwatch_canary_front_end_screenshots.eventbridge_rule_arn } } } + module "lambda_canary_notification_security_group" { source = "terraform-aws-modules/security-group/aws" version = "5.1.0" diff --git a/terraform/modules/cloud-watch-canary/cloudwatch.alarm.tf b/terraform/modules/cloud-watch-canary/cloudwatch.alarm.tf deleted file mode 100644 index 70759dc4..00000000 --- a/terraform/modules/cloud-watch-canary/cloudwatch.alarm.tf +++ /dev/null @@ -1,23 +0,0 @@ -module "cloudwatch_alarm" { - source = "terraform-aws-modules/cloudwatch/aws//modules/metric-alarm" - version = "5.3.1" - - alarm_name = "${var.name}-failed-alarm" - alarm_description = jsonencode( - { - "description": "Alarm for SNS", - "s3_prefix": "canary/eu-west-2/${var.name}" - } - ) - alarm_actions = [module.sns_topic_alarm.topic_arn] - comparison_operator = "LessThanOrEqualToThreshold" - evaluation_periods = 1 - threshold = 99 - period = var.timeout_in_seconds - dimensions = { - CanaryName = aws_synthetics_canary.this.name - } - namespace = "CloudWatchSynthetics" - metric_name = "SuccessPercent" - statistic = "Sum" -} diff --git a/terraform/modules/cloud-watch-canary/eventbridge.tf b/terraform/modules/cloud-watch-canary/eventbridge.tf new file mode 100644 index 00000000..41738a57 --- /dev/null +++ b/terraform/modules/cloud-watch-canary/eventbridge.tf @@ -0,0 +1,23 @@ +module "eventbridge" { + source = "terraform-aws-modules/eventbridge/aws" + create_bus = false + role_name = "${var.name}-eventbridge-role" + + rules = { + "${var.name}" = { + description = "Capture canary run fail" + event_pattern = jsonencode({ + source : ["aws.synthetics"] + }) + } + } + + targets = { + "${var.name}" = [ + { + name = var.lambda_function_notification_name + arn = var.lambda_function_notification_arn + }, + ] + } +} diff --git a/terraform/modules/cloud-watch-canary/outputs.tf b/terraform/modules/cloud-watch-canary/outputs.tf index dc4e4a19..da33be97 100644 --- a/terraform/modules/cloud-watch-canary/outputs.tf +++ b/terraform/modules/cloud-watch-canary/outputs.tf @@ -1,11 +1,11 @@ -output "sns_topic_arn" { - value = module.sns_topic_alarm.topic_arn -} - output "name" { value = var.name } output "artifact_s3_location" { value = aws_synthetics_canary.this.artifact_s3_location -} \ No newline at end of file +} + +output "eventbridge_rule_arn" { + value = module.eventbridge.eventbridge_rule_arns["${var.name}"] +} diff --git a/terraform/modules/cloud-watch-canary/sns.alarm.tf b/terraform/modules/cloud-watch-canary/sns.alarm.tf deleted file mode 100644 index 73852374..00000000 --- a/terraform/modules/cloud-watch-canary/sns.alarm.tf +++ /dev/null @@ -1,11 +0,0 @@ -module "sns_topic_alarm" { - source = "terraform-aws-modules/sns/aws" - name = "${var.name}-alarms" - - subscriptions = { - lambda = { - protocol = "lambda" - endpoint = var.lambda_function_notification_arn - } - } -} \ No newline at end of file diff --git a/terraform/modules/cloud-watch-canary/vars.tf b/terraform/modules/cloud-watch-canary/vars.tf index ac36bd71..bf5d6f72 100644 --- a/terraform/modules/cloud-watch-canary/vars.tf +++ b/terraform/modules/cloud-watch-canary/vars.tf @@ -54,3 +54,8 @@ variable "lambda_function_notification_arn" { description = "The ARN associated with Lambda function used to perform the notification trigger" type = string } + +variable "lambda_function_notification_name" { + description = "The name associated with Lambda function used to perform the notification trigger" + type = string +} From ce2457d503a2b7faa23e3f849e95c548c0b31274 Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Mon, 19 Aug 2024 10:44:43 +0100 Subject: [PATCH 49/59] Set notification lambda to consume event from eventbridge --- terraform/20-app/cloudwatch.canary.front-end.tf | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/terraform/20-app/cloudwatch.canary.front-end.tf b/terraform/20-app/cloudwatch.canary.front-end.tf index 7df84724..5249834f 100644 --- a/terraform/20-app/cloudwatch.canary.front-end.tf +++ b/terraform/20-app/cloudwatch.canary.front-end.tf @@ -11,10 +11,10 @@ module "cloudwatch_canary_front_end_screenshots" { vpc_id = module.vpc.vpc_id subnet_ids = module.vpc.private_subnets - schedule_expression = "rate(10 minutes)" - timeout_in_seconds = 600 - src_script_path = "canary-front-end-broken-links" - lambda_function_notification_arn = module.lambda_canary_notification.lambda_function_arn + schedule_expression = "rate(10 minutes)" + timeout_in_seconds = 600 + src_script_path = "canary-front-end-broken-links" + lambda_function_notification_arn = module.lambda_canary_notification.lambda_function_arn lambda_function_notification_name = module.lambda_canary_notification.lambda_function_name environment_variables = { From 37cfee4681adf7d8d0d897f066f6967c9544427e Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Mon, 19 Aug 2024 11:21:56 +0100 Subject: [PATCH 50/59] Trigger for failed canary runs only --- terraform/modules/cloud-watch-canary/eventbridge.tf | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/terraform/modules/cloud-watch-canary/eventbridge.tf b/terraform/modules/cloud-watch-canary/eventbridge.tf index 41738a57..8302f41d 100644 --- a/terraform/modules/cloud-watch-canary/eventbridge.tf +++ b/terraform/modules/cloud-watch-canary/eventbridge.tf @@ -7,7 +7,11 @@ module "eventbridge" { "${var.name}" = { description = "Capture canary run fail" event_pattern = jsonencode({ - source : ["aws.synthetics"] + source: ["aws.synthetics"], + detail: { + "canary-name": [aws_synthetics_canary.this.name] + "test-run-status": ["FAILED"] + } }) } } From 2bbde92529f8fe464f5df5fc88825c3b81837e1f Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Mon, 19 Aug 2024 15:00:47 +0100 Subject: [PATCH 51/59] Add `aws-sdk/client-synthetics` package --- .../package-lock.json | 314 ++++++++++++++++++ src/lambda-canary-notification/package.json | 1 + 2 files changed, 315 insertions(+) diff --git a/src/lambda-canary-notification/package-lock.json b/src/lambda-canary-notification/package-lock.json index 79cba15e..e0164204 100644 --- a/src/lambda-canary-notification/package-lock.json +++ b/src/lambda-canary-notification/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.556.0", "@aws-sdk/client-secrets-manager": "^3.624.0", + "@aws-sdk/client-synthetics": "^3.632.0", "@slack/web-api": "^7.3.1", "axios": "^1.7.2", "form-data": "^4.0.0" @@ -722,6 +723,319 @@ "node": ">=16.0.0" } }, + "node_modules/@aws-sdk/client-synthetics": { + "version": "3.632.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-synthetics/-/client-synthetics-3.632.0.tgz", + "integrity": "sha512-Ce1CXIDuoa8x6EH3tmrSUtzGzyJaNF5mTb9OA1rnaBYBOS3ObIROSB/JIe+wvod7B92s4PTZ2wvCur3DzwQSaw==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.632.0", + "@aws-sdk/client-sts": "3.632.0", + "@aws-sdk/core": "3.629.0", + "@aws-sdk/credential-provider-node": "3.632.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.632.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.632.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.2", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.14", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-synthetics/node_modules/@aws-sdk/client-sso": { + "version": "3.632.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.632.0.tgz", + "integrity": "sha512-iYWHiKBz44m3chCFvtvHnvCpL2rALzyr1e6tOZV3dLlOKtQtDUlPy6OtnXDu4y+wyJCniy8ivG3+LAe4klzn1Q==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.629.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.632.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.632.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.2", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.14", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-synthetics/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.632.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.632.0.tgz", + "integrity": "sha512-Oh1fIWaoZluihOCb/zDEpRTi+6an82fgJz7fyRBugyLhEtDjmvpCQ3oKjzaOhoN+4EvXAm1ZS/ZgpvXBlIRTgw==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.629.0", + "@aws-sdk/credential-provider-node": "3.632.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.632.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.632.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.2", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.14", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.632.0" + } + }, + "node_modules/@aws-sdk/client-synthetics/node_modules/@aws-sdk/client-sts": { + "version": "3.632.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.632.0.tgz", + "integrity": "sha512-Ss5cBH09icpTvT+jtGGuQlRdwtO7RyE9BF4ZV/CEPATdd9whtJt4Qxdya8BUnkWR7h5HHTrQHqai3YVYjku41A==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.632.0", + "@aws-sdk/core": "3.629.0", + "@aws-sdk/credential-provider-node": "3.632.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.632.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.632.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.2", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.14", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-synthetics/node_modules/@aws-sdk/core": { + "version": "3.629.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.629.0.tgz", + "integrity": "sha512-+/ShPU/tyIBM3oY1cnjgNA/tFyHtlWq+wXF9xEKRv19NOpYbWQ+xzNwVjGq8vR07cCRqy/sDQLWPhxjtuV/FiQ==", + "dependencies": { + "@smithy/core": "^2.3.2", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/property-provider": "^3.1.3", + "@smithy/protocol-http": "^4.1.0", + "@smithy/signature-v4": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/util-middleware": "^3.0.3", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-synthetics/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.632.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.632.0.tgz", + "integrity": "sha512-m6epoW41xa1ajU5OiHcmQHoGVtrbXBaRBOUhlCLZmcaqMLYsboM4iD/WZP8aatKEON5tTnVXh/4StV8D/+wemw==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.620.1", + "@aws-sdk/credential-provider-http": "3.622.0", + "@aws-sdk/credential-provider-process": "3.620.1", + "@aws-sdk/credential-provider-sso": "3.632.0", + "@aws-sdk/credential-provider-web-identity": "3.621.0", + "@aws-sdk/types": "3.609.0", + "@smithy/credential-provider-imds": "^3.2.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.632.0" + } + }, + "node_modules/@aws-sdk/client-synthetics/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.632.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.632.0.tgz", + "integrity": "sha512-cL8fuJWm/xQBO4XJPkeuZzl3XinIn9EExWgzpG48NRMKR5us1RI/ucv7xFbBBaG+r/sDR2HpYBIA3lVIpm1H3Q==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.620.1", + "@aws-sdk/credential-provider-http": "3.622.0", + "@aws-sdk/credential-provider-ini": "3.632.0", + "@aws-sdk/credential-provider-process": "3.620.1", + "@aws-sdk/credential-provider-sso": "3.632.0", + "@aws-sdk/credential-provider-web-identity": "3.621.0", + "@aws-sdk/types": "3.609.0", + "@smithy/credential-provider-imds": "^3.2.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-synthetics/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.632.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.632.0.tgz", + "integrity": "sha512-P/4wB6j7ym5QCPTL2xlMfvf2NcXSh+z0jmsZP4WW/tVwab4hvgabPPbLeEZDSWZ0BpgtxKGvRq0GSHuGeirQbA==", + "dependencies": { + "@aws-sdk/client-sso": "3.632.0", + "@aws-sdk/token-providers": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-synthetics/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.632.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.632.0.tgz", + "integrity": "sha512-yY/sFsHKwG9yzSf/DTclqWJaGPI2gPBJDCGBujSqTG1zlS7Ot4fqi91DZ6088BFWzbOorDzJFcAhAEFzc6LuQg==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.632.0", + "@smithy/protocol-http": "^4.1.0", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-synthetics/node_modules/@aws-sdk/util-endpoints": { + "version": "3.632.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.632.0.tgz", + "integrity": "sha512-LlYMU8pAbcEQphOpE6xaNLJ8kPGhklZZTVzZVpVW477NaaGgoGTMYNXTABYHcxeF5E2lLrxql9OmVpvr8GWN8Q==", + "dependencies": { + "@aws-sdk/types": "3.609.0", + "@smithy/types": "^3.3.0", + "@smithy/util-endpoints": "^2.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@aws-sdk/core": { "version": "3.623.0", "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.623.0.tgz", diff --git a/src/lambda-canary-notification/package.json b/src/lambda-canary-notification/package.json index f2b91e7e..5dd00521 100644 --- a/src/lambda-canary-notification/package.json +++ b/src/lambda-canary-notification/package.json @@ -17,6 +17,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.556.0", "@aws-sdk/client-secrets-manager": "^3.624.0", + "@aws-sdk/client-synthetics": "^3.632.0", "@slack/web-api": "^7.3.1", "axios": "^1.7.2", "form-data": "^4.0.0" From f52b08725d8d539403458c0c93684aaec54685b8 Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Mon, 19 Aug 2024 15:01:59 +0100 Subject: [PATCH 52/59] Allow notification lambda to call get the status of previous canary runs --- terraform/20-app/lambda.canary-notification.tf | 5 +++++ terraform/modules/cloud-watch-canary/outputs.tf | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/terraform/20-app/lambda.canary-notification.tf b/terraform/20-app/lambda.canary-notification.tf index 428c57ec..93e0d2b6 100644 --- a/terraform/20-app/lambda.canary-notification.tf +++ b/terraform/20-app/lambda.canary-notification.tf @@ -36,6 +36,11 @@ module "lambda_canary_notification" { actions = ["secretsmanager:GetSecretValue"], resources = [aws_secretsmanager_secret.slack_webhook_url.arn] } + get_recent_canary_runs = { + effect = "Allow", + actions = ["synthetics:GetCanaryRuns"], + resources = [module.cloudwatch_canary_front_end_screenshots.canary_arn] + } } create_current_version_allowed_triggers = false diff --git a/terraform/modules/cloud-watch-canary/outputs.tf b/terraform/modules/cloud-watch-canary/outputs.tf index da33be97..fa37f603 100644 --- a/terraform/modules/cloud-watch-canary/outputs.tf +++ b/terraform/modules/cloud-watch-canary/outputs.tf @@ -9,3 +9,7 @@ output "artifact_s3_location" { output "eventbridge_rule_arn" { value = module.eventbridge.eventbridge_rule_arns["${var.name}"] } + +output "canary_arn" { + value = aws_synthetics_canary.this.arn +} \ No newline at end of file From fc4452844ceef7bd83330493163ae78bdcdc30fa Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Mon, 19 Aug 2024 15:49:53 +0100 Subject: [PATCH 53/59] Send notification if canary run state transitioned from PASSED -> FAILED compared to most recent run --- src/lambda-canary-notification/index.js | 69 ++++++++- src/lambda-canary-notification/index.test.js | 140 +++++++++++++++++++ 2 files changed, 205 insertions(+), 4 deletions(-) diff --git a/src/lambda-canary-notification/index.js b/src/lambda-canary-notification/index.js index 165ff2d4..4fa4fb4e 100644 --- a/src/lambda-canary-notification/index.js +++ b/src/lambda-canary-notification/index.js @@ -6,6 +6,7 @@ const {WebClient} = require("@slack/web-api") const axios = require('axios'); const FormData = require('form-data'); const path = require('path'); +const {GetCanaryRunsCommand, SyntheticsClient} = require("@aws-sdk/client-synthetics"); /** * Gets the filename associated with the s3 key / file path @@ -17,6 +18,38 @@ function getFilename(filePath) { return path.basename(filePath); } + +/** + * Gets the recent canary runs + * + * @param {SyntheticsClient} syntheticsClient - An instance of the SyntheticsClient + * to use for sending the command. + * @param {string} canaryName - The name of the synthetics canary resource + * + * @returns {object} - The response from the synthetics service + */ +async function getPreviousCanaryRun(canaryName, syntheticsClient = new SyntheticsClient()) { + const input = { + "Name": canaryName, + "MaxResults": 1, + }; + const command = new GetCanaryRunsCommand(input); + return syntheticsClient.send(command); +} + + +/** + * Gets the state of the previous canary run from the `recentRuns` + * + * @param {object} recentRuns - Array of recent runs from the synthetics API + * + * @returns {object} - The state of the previous canary run. + * Can be "PASSED" or "FAILED" + */ +function getRunStateOfPreviousRun(recentRuns) { + return recentRuns.CanaryRuns[0].Status.State +} + /** * Gets the secret for the Slack webhook URL from SecretsManager * @@ -376,6 +409,30 @@ async function extractReport(folderContents, keyToSearchFor, bucketName, s3Clien return JSON.parse(jsonString); } +/** + * Compares the previous canary run to see if the notification is required and the status has changed from pass to fail. + * + * @param {object} event - The object passed down to the Lambda runtime on initialization. + * @param overriddenDependencies - Object used to override the default dependencies. + * + * @return {boolean} - true if the notification is required, false otherwise. + */ +async function determineIfNotificationRequired(event, overriddenDependencies = {}) { + const defaultDependencies = { + getPreviousCanaryRun, + getRunStateOfPreviousRun, + }; + const dependencies = {...defaultDependencies, ...overriddenDependencies} + + const canaryName = event.detail["canary-name"] + const recentRun = await dependencies.getPreviousCanaryRun(canaryName) + + const recentRunStatus = dependencies.getRunStateOfPreviousRun(recentRun) + const currentRunStatus = event.detail["test-run-status"] + + return (currentRunStatus === "FAILED" && recentRunStatus === "PASSED"); +} + /** * Main handler entrypoint for the Lambda runtime execution. * @@ -386,6 +443,7 @@ async function extractReport(folderContents, keyToSearchFor, bucketName, s3Clien */ async function handler(event, context, overriddenDependencies = {}) { const defaultDependencies = { + determineIfNotificationRequired, getSlackSecret, buildSlackClient, listFiles, @@ -399,13 +457,13 @@ async function handler(event, context, overriddenDependencies = {}) { const dependencies = {...defaultDependencies, ...overriddenDependencies} const bucketName = process.env.S3_CANARY_LOGS_BUCKET_NAME - const relevantFolder = getPathFromArtifactLocation(event.detail["artifact-location"]) - const runStatus = event.detail["test-run-status"] - if (runStatus === "PASSED") { - console.log('Canary run passed') + const isNotificationRequired = dependencies.determineIfNotificationRequired(event) + if (!isNotificationRequired) { + console.log('Notification not required') return } + const relevantFolder = getPathFromArtifactLocation(event.detail["artifact-location"]) const slackSecret = await dependencies.getSlackSecret() const slackClient = await dependencies.buildSlackClient(slackSecret.slack_token) @@ -427,6 +485,9 @@ async function handler(event, context, overriddenDependencies = {}) { module.exports = { handler, getFilename, + getPreviousCanaryRun, + getRunStateOfPreviousRun, + determineIfNotificationRequired, getSecret, getSlackSecret, listFiles, diff --git a/src/lambda-canary-notification/index.test.js b/src/lambda-canary-notification/index.test.js index 5b3ea5fa..6d3fae5f 100644 --- a/src/lambda-canary-notification/index.test.js +++ b/src/lambda-canary-notification/index.test.js @@ -5,6 +5,7 @@ const path = require('path'); const sinon = require("sinon"); const {GetSecretValueCommand} = require("@aws-sdk/client-secrets-manager"); const index = require("./index"); +const {GetCanaryRunsCommand} = require("@aws-sdk/client-synthetics"); // Mock dependencies jest.mock('axios'); @@ -513,6 +514,123 @@ describe('sendSlackPost', () => { }); }); + +describe('getPreviousCanaryRun', () => { + /** + * Given the name of the synthetics canary + * When `getPreviousCanaryRun()` is called + * Then the correct command is used when + * the `send` method is called from the `SyntheticsClient` + */ + test('Gets the recent canary run from Synthetics', async () => { + // Given + const fakeCanaryName = 'fake-canary-name' + const spySyntheticsClient = { + send: sinon.stub().resolves({}), + } + + // When + await index.getPreviousCanaryRun(fakeCanaryName, spySyntheticsClient); + + // Then + expect(spySyntheticsClient.send.calledWith(sinon.match.instanceOf(GetCanaryRunsCommand))).toBeTruthy() + const argsCalledWithSpy = spySyntheticsClient.send.firstCall.args[0].input; + expect(argsCalledWithSpy.Name).toEqual(fakeCanaryName); + expect(argsCalledWithSpy.MaxResults).toEqual(1); + }); +}) + +describe('getRunStateOfPreviousRun', () => { + /** + * Given the recent canary runs + * When `getRunStateOfPreviousRun()` is called + * Then the correct state is returned + */ + test('Extracts the state associated with the most recent canary run', async () => { + // Given + const recentRuns = { + "CanaryRuns": [ + { + "Name": "uhd-fake-env-display", + "Status": { + "State": "FAILED", + "StateReason": "", + "StateReasonCode": "" + }, + } + ], + } + + // When + const extractedStatus = index.getRunStateOfPreviousRun(recentRuns); + + // Then + expect(extractedStatus).toStrictEqual("FAILED") + }); +}) + +describe('determineIfNotificationIsRequired', () => { + const failedEvent = { + "detail-type": "Synthetics Canary TestRun Failure", + "source": "aws.synthetics", + "account": "123456789012", + "detail": { + "account-id": "123456789012", + "test-run-status": "FAILED", + } + }; + const passedEvent = { + "detail-type": "Synthetics Canary TestRun Failure", + "source": "aws.synthetics", + "account": "123456789012", + "detail": { + "account-id": "123456789012", + "test-run-status": "PASSED", + } + }; + + test.each([ + { + description: 'Returns true when run changed from PASSED to FAILED', + event: failedEvent, + previousRunStatus: 'PASSED', + expectedResult: true + }, + { + description: 'Returns false when run remained as FAILED between runs', + event: failedEvent, + previousRunStatus: 'FAILED', + expectedResult: false + }, + { + description: 'Returns false when run changed from FAILED to PASSED', + event: passedEvent, + previousRunStatus: 'FAILED', + expectedResult: false + }, + { + description: 'Returns false when run changed from PASSED to PASSED', + event: passedEvent, + previousRunStatus: 'PASSED', + expectedResult: false + } + ])('$description', async ({event, previousRunStatus, expectedResult}) => { + // Given + const mockedGetPreviousCanaryRun = sinon.stub(); + const mockedGetRunStateOfPreviousRun = sinon.stub().returns(previousRunStatus); + const injectedDependencies = { + getPreviousCanaryRun: mockedGetPreviousCanaryRun, + getRunStateOfPreviousRun: mockedGetRunStateOfPreviousRun + }; + + // When + const extractedBoolean = await index.determineIfNotificationRequired(event, injectedDependencies); + + // Then + expect(extractedBoolean).toBe(expectedResult); + }); +}); + describe('extractReport', () => { /** * Given a report key and an S3 client @@ -550,6 +668,7 @@ describe('extractReport', () => { }); describe('handler', () => { + let spyDetermineIfNotificationRequired let spyGetSlackSecret; let spyBuildSlackClient; let spyListFiles; @@ -561,6 +680,25 @@ describe('handler', () => { let spyUploadAllScreenshotsToSlackThread; let injectedDependencies; + const previousRun = { + "CanaryRuns": [ + { + "ArtifactS3Location": "uhd-fake-env-canary-logs/canary/eu-west-2/uhd-fake-env-display/2024/08/19/14/05-39-555", + "Name": "uhd-fake-env-display", + "Status": { + "State": "PASSED", + "StateReason": "", + "StateReasonCode": "" + }, + "Timeline": { + "Completed": "2024-08-19T14:10:28.342Z", + "Started": "2024-08-19T14:05:39.555Z" + } + } + ], + "NextToken": "2024-08-19T14:05:39.555Z" + } + const event = { "detail-type": "Synthetics Canary TestRun Successful", "source": "aws.synthetics", @@ -590,6 +728,7 @@ describe('handler', () => { const downloadResponses = ['response1', 'response2']; beforeEach(() => { + spyDetermineIfNotificationRequired = sinon.stub().resolves(true) spyGetSlackSecret = sinon.stub().resolves(slackSecret); spyBuildSlackClient = sinon.stub().resolves(slackClient); spyListFiles = sinon.stub().resolves(listedFiles); @@ -603,6 +742,7 @@ describe('handler', () => { spyUploadAllScreenshotsToSlackThread = sinon.stub().resolves(); injectedDependencies = { + determineIfNotificationRequired: spyDetermineIfNotificationRequired, getSlackSecret: spyGetSlackSecret, buildSlackClient: spyBuildSlackClient, listFiles: spyListFiles, From 7824bdc8963bcf9831b4802f5c0050ac8a23b17a Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Mon, 19 Aug 2024 16:08:55 +0100 Subject: [PATCH 54/59] Replace with HLC2 expression --- terraform/modules/cloud-watch-canary/eventbridge.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/terraform/modules/cloud-watch-canary/eventbridge.tf b/terraform/modules/cloud-watch-canary/eventbridge.tf index 8302f41d..4dcbba82 100644 --- a/terraform/modules/cloud-watch-canary/eventbridge.tf +++ b/terraform/modules/cloud-watch-canary/eventbridge.tf @@ -4,7 +4,7 @@ module "eventbridge" { role_name = "${var.name}-eventbridge-role" rules = { - "${var.name}" = { + (var.name) = { description = "Capture canary run fail" event_pattern = jsonencode({ source: ["aws.synthetics"], @@ -17,7 +17,7 @@ module "eventbridge" { } targets = { - "${var.name}" = [ + (var.name) = [ { name = var.lambda_function_notification_name arn = var.lambda_function_notification_arn From b7d31f2905b8d172d3a8dd02c9cd6dd94ec871ca Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Mon, 19 Aug 2024 16:33:49 +0100 Subject: [PATCH 55/59] Remove `"cloudwatch:PutMetricData"` policy for canary --- terraform/modules/cloud-watch-canary/iam.tf | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/terraform/modules/cloud-watch-canary/iam.tf b/terraform/modules/cloud-watch-canary/iam.tf index f928dba6..dda6bb53 100644 --- a/terraform/modules/cloud-watch-canary/iam.tf +++ b/terraform/modules/cloud-watch-canary/iam.tf @@ -32,16 +32,6 @@ module "iam_canary_policy" { Effect = "Allow", Resource = ["*"] }, - { - Action = ["cloudwatch:PutMetricData"], - Effect = "Allow", - Resource = ["*"], - Condition = { - StringEquals = { - "cloudwatch:namespace" = "CloudWatchSynthetics" - } - } - }, { Action = [ "ec2:CreateNetworkInterface", From a7264698e41413b61014ce3db406e18d21c8bb1d Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Mon, 19 Aug 2024 20:34:44 +0100 Subject: [PATCH 56/59] Delay next page for 500ms --- src/canary-front-end-broken-links/index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/canary-front-end-broken-links/index.js b/src/canary-front-end-broken-links/index.js index ee7907b8..e22f9d87 100644 --- a/src/canary-front-end-broken-links/index.js +++ b/src/canary-front-end-broken-links/index.js @@ -5,6 +5,9 @@ const SyntheticsLink = require('SyntheticsLink'); const syntheticsLogHelper = require('SyntheticsLogHelper'); const syntheticsConfiguration = synthetics.getConfiguration(); + +const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); + function extractUrlsFromSitemap(xml) { const urlRegex = /(.*?)<\/loc>/g; const urls = []; @@ -161,6 +164,7 @@ const webCrawlerBlueprint = async function () { }); while (synLinks.length > 0) { + await delay(500) let link = synLinks.shift(); let nav_url = link.getUrl(); let sanitized_url = syntheticsLogHelper.getSanitizedUrl(nav_url); From cafa6c50ed9bdb2da9284d256909acc54b797b3f Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Wed, 21 Aug 2024 15:30:52 +0100 Subject: [PATCH 57/59] Remove block to resursively find other links on page --- src/canary-front-end-broken-links/index.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/canary-front-end-broken-links/index.js b/src/canary-front-end-broken-links/index.js index e22f9d87..2bf86827 100644 --- a/src/canary-front-end-broken-links/index.js +++ b/src/canary-front-end-broken-links/index.js @@ -234,19 +234,6 @@ const webCrawlerBlueprint = async function () { } catch (e) { synthetics.addExecutionError('Unable to add link to broken link checker report.', e); } - - // If current link was successfully loaded, grab more hyperlinks from this page. - if (response && response.status() && response.status() < 400 && exploredUrls.length < limit) { - try { - let moreLinks = await grabLinks(page, sanitized_url, exploredUrls); - if (moreLinks && moreLinks.length > 0) { - synLinks = synLinks.concat(moreLinks); - } - } catch (e) { - canaryError = "Unable to grab urls on page: " + sanitized_url + ". " + e; - log.error(canaryError); - } - } } try { From 6db2ff84f1b2f42238e44ba02cbeccb19a677a4c Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Wed, 21 Aug 2024 15:39:40 +0100 Subject: [PATCH 58/59] Remove redundant `grabLinks()` function --- src/canary-front-end-broken-links/index.js | 57 ---------------------- 1 file changed, 57 deletions(-) diff --git a/src/canary-front-end-broken-links/index.js b/src/canary-front-end-broken-links/index.js index 2bf86827..bc335c93 100644 --- a/src/canary-front-end-broken-links/index.js +++ b/src/canary-front-end-broken-links/index.js @@ -49,63 +49,6 @@ const captureDestinationPageScreenshotOnFailure = true; // Increase or decrease based on complexity of your website. const numOfLinksToReLaunchBrowser = 50; -// async function used to grab urls from page -// fetch hrefs from DOM -const grabLinks = async function (page, sourceUrl, exploredUrls) { - let grabbedLinks = []; - - const jsHandle = await page.evaluateHandle(() => { - return document.getElementsByTagName('a'); - }); - - const numberOfLinks = await page.evaluate(e => e.length, jsHandle); - - for (let i = 0; i < numberOfLinks; i++) { - let element = await page.evaluate((jsHandle, i, captureSourcePageScreenshot, exploredUrls) => { - let element = jsHandle[i]; - let url = String(element.href).trim(); - // Condition for grabbing a link - if (url != null && url.length > 0 && !exploredUrls.includes(url) && (url.startsWith('http') || url.startsWith('https'))) { - let text = element.text ? element.text.trim() : ''; - let originalBorderProp = element.style.border; - // Annotate this anchor element for source page screenshot. - if (captureSourcePageScreenshot) { - // Use color of your choosing for annotation. - element.style.border = '3px solid #e67e22'; - element.scrollIntoViewIfNeeded(); - } - return {text, url, originalBorderProp}; - } - }, jsHandle, i, captureSourcePageScreenshot, exploredUrls); - - if (element) { - let url = element.url; - let originalBorderProp = element.originalBorderProp; - exploredUrls.push(url); - - let sourcePageScreenshotResult; - if (captureSourcePageScreenshot) { - sourcePageScreenshotResult = await takeScreenshot(getFileName(url), "sourcePage"); - - // Reset css to original - await page.evaluate((jsHandle, i, originalBorderProp) => { - let element = jsHandle[i]; - element.style.border = originalBorderProp; - }, jsHandle, i, originalBorderProp); - } - - let link = new SyntheticsLink(url).withParentUrl(sourceUrl).withText(element.text); - link.addScreenshotResult(sourcePageScreenshotResult); - grabbedLinks.push(link); - - if (exploredUrls.length >= limit) { - break; - } - } - } - return grabbedLinks; -} - // Take synthetics screenshot const takeScreenshot = async function (fileName, suffix) { try { From f50aa0a63729f69c10d7b9d203855a5c1b81e658 Mon Sep 17 00:00:00 2001 From: A-Ashiq Date: Thu, 24 Oct 2024 10:59:09 +0100 Subject: [PATCH 59/59] Refresh lock file --- terraform/20-app/.terraform.lock.hcl | 192 +++++++++++++-------------- 1 file changed, 96 insertions(+), 96 deletions(-) diff --git a/terraform/20-app/.terraform.lock.hcl b/terraform/20-app/.terraform.lock.hcl index 4579666e..17f783b9 100644 --- a/terraform/20-app/.terraform.lock.hcl +++ b/terraform/20-app/.terraform.lock.hcl @@ -2,135 +2,135 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/archive" { - version = "2.5.0" + version = "2.6.0" hashes = [ - "h1:GyV//bFbFWEll/6XafMvSUCmJL+r7wDDz222dMEmV3c=", - "h1:HXf8h8Z4JYEkBND/JiqC+CjluKqifKoDGrL1IsRo15M=", - "h1:OTk41JfiDc1TVFTcRZ//4+jwPBIcWHXOwN29mjdOyug=", - "zh:3b5774d20e87058d6d67d9ad4ce3fc4a5f7ea7748d345fa6721e24a0cbb0a3d4", - "zh:3b94e706ac0f5151880ccc9e63d33c4113361f27e64224a942caa04a5a19cd44", + "h1:Ou6XKWvpo7IYgZnrFJs5MKzMqQMEYv8Z2iHSJ2mmnFw=", + "h1:rYAubRk7UHC/fzYqFV/VHc+7VIY01ugCxauyTYCNf9E=", + "h1:upAbF0KeKLAs3UImwwp5veC7jRcLnpKWVjkbd4ziWhM=", + "zh:29273484f7423b7c5b3f5df34ccfc53e52bb5e3d7f46a81b65908e7a8fd69072", + "zh:3cba58ec3aea5f301caf2acc31e184c55d994cc648126cac39c63ae509a14179", + "zh:55170cd17dbfdea842852c6ae2416d057fec631ba49f3bb6466a7268cd39130e", + "zh:7197db402ba35631930c3a4814520f0ebe980ae3acb7f8b5a6f70ec90dc4a388", "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:7d7201858fa9376029818c9d017b4b53a933cea75480306b1122663d1e8eea2b", - "zh:8c8c7537978adf12271fe143f93b3587bb5dbabf8202ff49d0e3955b7bddc24b", - "zh:a5942584665a2689e73f3a3c43296adeaeb7e8698631d157419aa931ff856907", - "zh:a63673abdba624d60c84b819184fe86422bdbdf6bc73f68d903a7191aed32c00", - "zh:bcd1586cc32b263265e09e78f56dba3a6b6b19f5371c099a9d7a1bfe0b0667cc", - "zh:cc9e70e186e4dcef60208b4a64b42e6813b197e21ea106a96bb4eb23b54c3e44", - "zh:d4c8a0f69412892507a2c9ec0e334bcc2812a54b81212420d4f2c96ef58f713a", - "zh:e91e6d90bbc15252310eca6400d4188b29260aab0539480a3fc7b45e4d19c446", - "zh:fc468449c0dbda56aae6cb924e4a67578d18504b5b06e8989783182c6b4a5f73", + "zh:8bf7fe0915d7fb152a3a6b9162614d2ec82749a06dba13fab3f98d33c020ec4f", + "zh:8ce811844fd53adb0dabc9a541f8cb43aacfa7d8e39324e4bd3592b3428f5bfb", + "zh:bca795bca815b8ac90e3054c0a9ab1ccfb16eedbb3418f8ad473fc5ad6bf0ef7", + "zh:d9355a18df5a36cf19580748b23249de2eb445c231c36a353709f8f40a6c8432", + "zh:dc32cc32cfd8abf8752d34f2a783de0d3f7200c573b885ecb64ece5acea173b4", + "zh:ef498e20391bf7a280d0fd6fd6675621c85fbe4e92f0f517ae4394747db89bde", + "zh:f2bc5226c765b0c8055a7b6207d0fe1eb9484e3ec8880649d158827ac6ed3b22", ] } provider "registry.terraform.io/hashicorp/aws" { - version = "5.60.0" - constraints = ">= 3.29.0, >= 3.74.0, >= 4.0.0, >= 4.66.1, >= 5.0.0, >= 5.12.0, >= 5.25.0, >= 5.27.0, >= 5.30.0, >= 5.32.0, >= 5.37.0, >= 5.46.0, >= 5.49.0, >= 5.53.0, >= 5.58.0, 5.60.0" + version = "5.62.0" + constraints = ">= 3.29.0, >= 3.74.0, >= 4.0.0, >= 4.66.1, >= 5.0.0, >= 5.12.0, >= 5.25.0, >= 5.27.0, >= 5.32.0, >= 5.37.0, >= 5.46.0, >= 5.49.0, >= 5.58.0, >= 5.61.0, 5.62.0" hashes = [ - "h1:Ou/WgUdyL4dSzfO1U5J0Z7i4FS4QRBVxqVWFFnRl0Fk=", - "h1:msnFtzhM9fQgi5ePG7Skt5DvnqOiWqMSxCNBred/hso=", - "h1:p9+40kdklLTJLQ/y7wxNjuKxUK8AVB4L9424NGNK4rY=", - "zh:08f49c9eb865e136a55dda3eb2b790f6d55cdac49f6638391dbea4b865cf307b", - "zh:090dd8b40ebf0f8e9ea05b9a142add9caeb7988d3d96c5c112e8c67c0edf566f", - "zh:30f336af1b4f0824fce2cc6e81af0986b325b135436c9d892d081e435aeed67e", - "zh:338195ca3b41249874110253412d8913f770c22294af05799ea1e343050906f5", - "zh:3a8a45b17750b01192a0fbeeed0d05c2c04840344d78d5e3233b3ecbeec17a1c", - "zh:486efe72d39f0736d9b7e00e5b889288264458a57aa0cff2d75688d6db372ee5", - "zh:5fdccc448a085fea8ecfae43ae326840abfcdf1a0aa8b8c79dd466392aa5cc3a", - "zh:9521639755cd07ec7efde86a534770e436e16a93692d070a00f6419c1038d59c", + "h1:8tevkFG+ea/sNZYiQ2GQ02hknPcWBukxkrpjRCodQC0=", + "h1:X3LAZdkVhb/77gTlhPwKYCA9oblBCSu866fZDDOojPY=", + "h1:uzpbH5xjF0BIghmA9lCvg/bbEr//DhcgQkdQq50gcek=", + "zh:1f366cbcda72fb123015439a42ab19f96e10ce4edb404273f4e1b7e06da20b73", + "zh:25f098454a34b483279e0382b24b4f42e51c067222c6e797eda5d3ec33b9beb1", + "zh:4b59d48b527e3cefd73f196853bfc265b3e1e57b55c1c8a2d12ff6e3534b4f07", + "zh:7bb88c1ca95e2b3f0f1fe8636925133b9813fc5b137cc467ba6a233ddf4b360e", + "zh:8a93dece40e816c92647e762839d0370e9cad2aa21dc4ca95baee9385f116459", + "zh:8dfe82c55ab8f633c1e2a39c687e9ca8c892d1c2005bf5166ac396ce868ecd05", "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:c2fb9240a069da9f51e7379e76c3dfaad15a97430c2e32708a7d18345434e310", - "zh:daba836b89537dfa72bb8c77e88850c20fda2a3d0f5b3803cd3d6da0ce283e3e", - "zh:db7e0755ed120ed8311f6663f49aa7157da5072b906727db3a6c47d64e0b82c6", - "zh:ea5e3fca5197639c4ad1415ca96de2924a351ecd1a885dd9184843d5eec18dbb", - "zh:f3f322951d311e45a47361f24790a90a0b8ba6d3829a00c4066a361960d2ecef", - "zh:f48b44f4887d4b51a1406057f15f1e2161cb02b271b2659349958904c678e91c", + "zh:a754952d69b4860480d5207390e3ab42350c964dbca9a5ac0c6912dd24b4c11d", + "zh:b2a4dbf4abee0e9ec18c5d323b99defdcd3c681f8c4306fb6e02cff7de038f85", + "zh:b57d84be258b571c04271015f03858ab215768b82e47c11ecd86e789d577030a", + "zh:be811b03289407c8d59e6b199bf16e6071165565ffe502148172d0886cf849c4", + "zh:d4144c7366c840eff1ac15ba13d96063f798f0983d24053a832362033624fe6f", + "zh:d88612856d453c4e10c49c76e4ef522b7d068b4f7c3e2e0b03dd74540986eecd", + "zh:e8bd231a5d0786cc4aab8471bb6dabd5a5df1c598afda077a9f27987ada57b67", + "zh:ffb40a66b4d000a8ee4c54227eeb998f887ad867419c3af7d3981587788de074", ] } provider "registry.terraform.io/hashicorp/external" { - version = "2.3.3" + version = "2.3.4" constraints = ">= 1.0.0" hashes = [ - "h1:H+3QlVPs/7CDa3I4KU/a23wYeGeJxeBlgvR7bfK1t1w=", - "h1:Qi72kOSrEYgEt5itloFhDfmiFZ7wnRy3+F74XsRuUOw=", - "h1:gShzO1rJtADK9tDZMvMgjciVAzsBh39LNjtThCwX1Hg=", - "zh:03d81462f9578ec91ce8e26f887e34151eda0e100f57e9772dbea86363588239", - "zh:37ec2a20f6a3ec3a0fd95d3f3de26da6cb9534b30488bc45723e118a0911c0d8", - "zh:4eb5b119179539f2749ce9de0e1b9629d025990f062f4f4dddc161562bb89d37", - "zh:5a31bb58414f41bee5e09b939012df5b88654120b0238a89dfd6691ba197619a", - "zh:6221a05e52a6a2d4f520ffe7cbc741f4f6080e0855061b0ed54e8be4a84eb9b7", + "h1:U6W8rgrdmR2pZ2cicFoGOSQ4GXuIf/4EK7s0vTJN7is=", + "h1:XWkRZOLKMjci9/JAtE8X8fWOt7A4u+9mgXSUjc4Wuyo=", + "h1:cCabxnWQ5fX1lS7ZqgUzsvWmKZw9FA7NRxAZ94vcTcc=", + "zh:037fd82cd86227359bc010672cd174235e2d337601d4686f526d0f53c87447cb", + "zh:0ea1db63d6173d01f2fa8eb8989f0809a55135a0d8d424b08ba5dabad73095fa", + "zh:17a4d0a306566f2e45778fbac48744b6fd9c958aaa359e79f144c6358cb93af0", + "zh:298e5408ab17fd2e90d2cd6d406c6d02344fe610de5b7dae943a58b958e76691", + "zh:38ecfd29ee0785fd93164812dcbe0664ebbe5417473f3b2658087ca5a0286ecb", + "zh:59f6a6f31acf66f4ea3667a555a70eba5d406c6e6d93c2c641b81d63261eeace", "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:8bb068496b4679bef625e4710d9f3432e301c3a56602271f04e60eadf7f8a94c", - "zh:94742aa5378bab626ce34f79bcef6a373e4f86ea7a8b762e9f71270a899e0d00", - "zh:a485831b5a525cd8f40e8982fa37da40ff70b1ae092c8b755fcde123f0b1238d", - "zh:a647ff16d071eabcabd87ea8183eb90a775a0294ddd735d742075d62fff09193", - "zh:b74710c5954aaa3faf262c18d36a8c2407862d9f842c63e7fa92fa4de3d29df6", - "zh:fa73d83edc92af2e551857594c2232ba6a9e3603ad34b0a5940865202c08d8d7", + "zh:ad0279dfd09d713db0c18469f585e58d04748ca72d9ada83883492e0dd13bd58", + "zh:c69f66fd21f5e2c8ecf7ca68d9091c40f19ad913aef21e3ce23836e91b8cbb5f", + "zh:d4a56f8c48aa86fc8e0c233d56850f5783f322d6336f3bf1916e293246b6b5d4", + "zh:f2b394ebd4af33f343835517e80fc876f79361f4688220833bc3c77655dd2202", + "zh:f31982f29f12834e5d21e010856eddd19d59cd8f449adf470655bfd19354377e", ] } provider "registry.terraform.io/hashicorp/local" { - version = "2.5.1" - constraints = ">= 1.0.0, 2.5.1" + version = "2.5.2" + constraints = ">= 1.0.0, 2.5.2" hashes = [ - "h1:/GAVA/xheGQcbOZEq0qxANOg+KVLCA7Wv8qluxhTjhU=", - "h1:8oTPe2VUL6E2d3OcrvqyjI4Nn/Y/UEQN26WLk5O/B0g=", - "h1:tjcGlQAFA0kmQ4vKkIPPUC4it1UYxLbg4YvHOWRAJHA=", - "zh:0af29ce2b7b5712319bf6424cb58d13b852bf9a777011a545fac99c7fdcdf561", - "zh:126063ea0d79dad1f68fa4e4d556793c0108ce278034f101d1dbbb2463924561", - "zh:196bfb49086f22fd4db46033e01655b0e5e036a5582d250412cc690fa7995de5", - "zh:37c92ec084d059d37d6cffdb683ccf68e3a5f8d2eb69dd73c8e43ad003ef8d24", - "zh:4269f01a98513651ad66763c16b268f4c2da76cc892ccfd54b401fff6cc11667", - "zh:51904350b9c728f963eef0c28f1d43e73d010333133eb7f30999a8fb6a0cc3d8", - "zh:73a66611359b83d0c3fcba2984610273f7954002febb8a57242bbb86d967b635", + "h1:IyFbOIO6mhikFNL/2h1iZJ6kyN3U00jgkpCLUCThAfE=", + "h1:JlMZD6nYqJ8sSrFfEAH0Vk/SL8WLZRmFaMUF9PJK5wM=", + "h1:p99F1AoV9z51aJ4EdItxz/vLwWIyhx/0Iw7L7sWSH1o=", + "zh:136299545178ce281c56f36965bf91c35407c11897f7082b3b983d86cb79b511", + "zh:3b4486858aa9cb8163378722b642c57c529b6c64bfbfc9461d940a84cd66ebea", + "zh:4855ee628ead847741aa4f4fc9bed50cfdbf197f2912775dd9fe7bc43fa077c0", + "zh:4b8cd2583d1edcac4011caafe8afb7a95e8110a607a1d5fb87d921178074a69b", + "zh:52084ddaff8c8cd3f9e7bcb7ce4dc1eab00602912c96da43c29b4762dc376038", + "zh:71562d330d3f92d79b2952ffdda0dad167e952e46200c767dd30c6af8d7c0ed3", "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:7ae387993a92bcc379063229b3cce8af7eaf082dd9306598fcd42352994d2de0", - "zh:9e0f365f807b088646db6e4a8d4b188129d9ebdbcf2568c8ab33bddd1b82c867", - "zh:b5263acbd8ae51c9cbffa79743fbcadcb7908057c87eb22fd9048268056efbc4", - "zh:dfcd88ac5f13c0d04e24be00b686d069b4879cc4add1b7b1a8ae545783d97520", + "zh:805f81ade06ff68fa8b908d31892eaed5c180ae031c77ad35f82cb7a74b97cf4", + "zh:8b6b3ebeaaa8e38dd04e56996abe80db9be6f4c1df75ac3cccc77642899bd464", + "zh:ad07750576b99248037b897de71113cc19b1a8d0bc235eb99173cc83d0de3b1b", + "zh:b9f1c3bfadb74068f5c205292badb0661e17ac05eb23bfe8bd809691e4583d0e", + "zh:cc4cbcd67414fefb111c1bf7ab0bc4beb8c0b553d01719ad17de9a047adff4d1", ] } provider "registry.terraform.io/hashicorp/null" { - version = "3.2.2" + version = "3.2.3" constraints = ">= 2.0.0" hashes = [ - "h1:IMVAUHKoydFrlPrl9OzasDnw/8ntZFerCC9iXw1rXQY=", - "h1:vWAsYRd7MjYr3adj8BVKRohVfHpWQdvkIwUQ2Jf5FVM=", - "h1:zT1ZbegaAYHwQa+QwIFugArWikRJI9dqohj8xb0GY88=", - "zh:3248aae6a2198f3ec8394218d05bd5e42be59f43a3a7c0b71c66ec0df08b69e7", - "zh:32b1aaa1c3013d33c245493f4a65465eab9436b454d250102729321a44c8ab9a", - "zh:38eff7e470acb48f66380a73a5c7cdd76cc9b9c9ba9a7249c7991488abe22fe3", - "zh:4c2f1faee67af104f5f9e711c4574ff4d298afaa8a420680b0cb55d7bbc65606", - "zh:544b33b757c0b954dbb87db83a5ad921edd61f02f1dc86c6186a5ea86465b546", - "zh:696cf785090e1e8cf1587499516b0494f47413b43cb99877ad97f5d0de3dc539", - "zh:6e301f34757b5d265ae44467d95306d61bef5e41930be1365f5a8dcf80f59452", + "h1:+AnORRgFbRO6qqcfaQyeX80W0eX3VmjadjnUFUJTiXo=", + "h1:I0Um8UkrMUb81Fxq/dxbr3HLP2cecTH2WMJiwKSrwQY=", + "h1:nKUqWEza6Lcv3xRlzeiRQrHtqvzX1BhIzjaOVXRYQXQ=", + "zh:22d062e5278d872fe7aed834f5577ba0a5afe34a3bdac2b81f828d8d3e6706d2", + "zh:23dead00493ad863729495dc212fd6c29b8293e707b055ce5ba21ee453ce552d", + "zh:28299accf21763ca1ca144d8f660688d7c2ad0b105b7202554ca60b02a3856d3", + "zh:55c9e8a9ac25a7652df8c51a8a9a422bd67d784061b1de2dc9fe6c3cb4e77f2f", + "zh:756586535d11698a216291c06b9ed8a5cc6a4ec43eee1ee09ecd5c6a9e297ac1", "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:913a929070c819e59e94bb37a2a253c228f83921136ff4a7aa1a178c7cce5422", - "zh:aa9015926cd152425dbf86d1abdbc74bfe0e1ba3d26b3db35051d7b9ca9f72ae", - "zh:bb04798b016e1e1d49bcc76d62c53b56c88c63d6f2dfe38821afef17c416a0e1", - "zh:c23084e1b23577de22603cff752e59128d83cfecc2e6819edadd8cf7a10af11e", + "zh:9d5eea62fdb587eeb96a8c4d782459f4e6b73baeece4d04b4a40e44faaee9301", + "zh:a6355f596a3fb8fc85c2fb054ab14e722991533f87f928e7169a486462c74670", + "zh:b5a65a789cff4ada58a5baffc76cb9767dc26ec6b45c00d2ec8b1b027f6db4ed", + "zh:db5ab669cf11d0e9f81dc380a6fdfcac437aea3d69109c7aef1a5426639d2d65", + "zh:de655d251c470197bcbb5ac45d289595295acb8f829f6c781d4a75c8c8b7c7dd", + "zh:f5c68199f2e6076bce92a12230434782bf768103a427e9bb9abee99b116af7b5", ] } provider "registry.terraform.io/hashicorp/random" { - version = "3.6.2" - constraints = "3.6.2" + version = "3.6.3" + constraints = "3.6.3" hashes = [ - "h1:R5qdQjKzOU16TziCN1vR3Exr/B+8WGK80glLTT4ZCPk=", - "h1:VavG5unYCa3SYISMKF9pzc3718M0bhPlcbUZZGl7wuo=", - "h1:wmG0QFjQ2OfyPy6BB7mQ57WtoZZGGV07uAPQeDmIrAE=", - "zh:0ef01a4f81147b32c1bea3429974d4d104bbc4be2ba3cfa667031a8183ef88ec", - "zh:1bcd2d8161e89e39886119965ef0f37fcce2da9c1aca34263dd3002ba05fcb53", - "zh:37c75d15e9514556a5f4ed02e1548aaa95c0ecd6ff9af1119ac905144c70c114", - "zh:4210550a767226976bc7e57d988b9ce48f4411fa8a60cd74a6b246baf7589dad", - "zh:562007382520cd4baa7320f35e1370ffe84e46ed4e2071fdc7e4b1a9b1f8ae9b", - "zh:5efb9da90f665e43f22c2e13e0ce48e86cae2d960aaf1abf721b497f32025916", - "zh:6f71257a6b1218d02a573fc9bff0657410404fb2ef23bc66ae8cd968f98d5ff6", + "h1:Fnaec9vA8sZ8BXVlN3Xn9Jz3zghSETIKg7ch8oXhxno=", + "h1:f6jXn4MCv67kgcofx9D49qx1ZEBv8oyvwKDMPBr0A24=", + "h1:zG9uFP8l9u+yGZZvi5Te7PV62j50azpgwPunq2vTm1E=", + "zh:04ceb65210251339f07cd4611885d242cd4d0c7306e86dda9785396807c00451", + "zh:448f56199f3e99ff75d5c0afacae867ee795e4dfda6cb5f8e3b2a72ec3583dd8", + "zh:4b4c11ccfba7319e901df2dac836b1ae8f12185e37249e8d870ee10bb87a13fe", + "zh:4fa45c44c0de582c2edb8a2e054f55124520c16a39b2dfc0355929063b6395b1", + "zh:588508280501a06259e023b0695f6a18149a3816d259655c424d068982cbdd36", + "zh:737c4d99a87d2a4d1ac0a54a73d2cb62974ccb2edbd234f333abd079a32ebc9e", "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:9647e18f221380a85f2f0ab387c68fdafd58af6193a932417299cdcae4710150", - "zh:bb6297ce412c3c2fa9fec726114e5e0508dd2638cad6a0cb433194930c97a544", - "zh:f83e925ed73ff8a5ef6e3608ad9225baa5376446349572c2449c0c0b3cf184b7", - "zh:fbef0781cb64de76b1df1ca11078aecba7800d82fd4a956302734999cfd9a4af", + "zh:a357ab512e5ebc6d1fda1382503109766e21bbfdfaa9ccda43d313c122069b30", + "zh:c51bfb15e7d52cc1a2eaec2a903ac2aff15d162c172b1b4c17675190e8147615", + "zh:e0951ee6fa9df90433728b96381fb867e3db98f66f735e0c3e24f8f16903f0ad", + "zh:e3cdcb4e73740621dabd82ee6a37d6cfce7fee2a03d8074df65086760f5cf556", + "zh:eff58323099f1bd9a0bec7cb04f717e7f1b2774c7d612bf7581797e1622613a0", ] }