diff --git a/lib/bitcoin-core/README.md b/lib/bitcoin-core/README.md new file mode 100644 index 00000000..0d0da217 --- /dev/null +++ b/lib/bitcoin-core/README.md @@ -0,0 +1,444 @@ +## Sample AWS Blockchain Node Runner app for Bitcoin Nodes + +| Contributed by | +|:--------------------------------:| +| [Simon Goldberg](https://github.com/racket2000)| + +### Overview + +This guide walks you through deploying a Bitcoin Core mainnet node in a **Virtual Private Cloud (VPC)** using **Docker**, leveraging **AWS Secrets Manager** for secure credential handling. This configuration ensures robust security and performance while optimizing data transfer costs. + +--- + +## Well-Architected + +
+Review pros and cons of this solution. + +### Well-Architected Checklist + +This is the Well-Architected checklist for **Bitcoin Core node implementation** of the AWS Blockchain Node Runner app. This checklist takes into account questions from the [AWS Well-Architected Framework](https://aws.amazon.com/architecture/well-architected/) which are relevant to this workload. Please feel free to add more checks from the framework if required for your workload. + +| Pillar | Control | Question/Check | Remarks | +|:------------------------|:----------------------------------|:---------------------------------------------------------------------------------|:-----------------| +| Security | Network protection | Are there unnecessary open ports in security groups? | Port 8332 (RPC) is restricted to the VPC. | +| | | Traffic inspection | Optional: VPC Flow Logs or traffic mirroring can be enabled for deeper inspection. | +| | Compute protection | Reduce attack surface | This solution uses Amazon Linux 2 AMI. No SSH access is enabled; SSM is used. | +| | | Enable people to perform actions at a distance | This solution uses AWS Systems Manager Session Manager. | +| | Data protection at rest | Use encrypted Amazon Elastic Block Store (Amazon EBS) volumes | Encrypted Amazon EBS volumes are used. | +| | Data protection in transit | Use TLS | The AWS Application Load balancer currently uses HTTP listener. Create HTTPS listener with self signed certificate if TLS is desired. | +| | Authorization and access control | Use instance profile with Amazon Elastic Compute Cloud (Amazon EC2) instances | AWS IAM role is attached to the EC2 instance. | +| | | Following principle of least privilege access | IAM privileges are scoped down to what is necessary. | +| | Application security | Security focused development practices | `cdk-nag` is used with appropriate suppressions. | +| Cost optimization | Service selection | Use cost effective resources | Cost efficient T3 instances provide a baseline level of CPU performance with the ability to burst CPU usage at any time for as long as required. T3 instances are designed for applications with moderate CPU usage that experience temporary spikes in use. | +| Reliability | Resiliency implementation | Withstand component failures | Single node deployment. Can be extended with backup nodes and monitoring. | +| | Resource monitoring | How are workload resources monitored? | Amazon CloudWatch Dashboards track CPU, memory, disk, network, and block height. | +| Performance efficiency | Compute selection | How is compute solution selected? | Compute solution is selected based on performance needs and budget. | +| | Storage selection | How is storage solution selected? | EBS volumes (e.g. gp3 or io2) are selected for consistent throughput and IOPS. | +| Operational excellence | Workload health | How is health of workload determined? | Health is tracked using CloudWatch custom metrics including block height. | +| Sustainability | Hardware & services | Select most efficient hardware for your workload | T3A instances offer efficient memory utilization, reducing power and cost. | + +
+ +### Getting Started + +#### Open AWS CloudShell + +To begin, ensure you login to your AWS account with permissions to create and modify resources in IAM, EC2, EBS, VPC, S3, KMS, and Secrets Manager. + +From the AWS Management Console, open the [AWS CloudShell](https://docs.aws.amazon.com/cloudshell/latest/userguide/welcome.html), a web-based shell environment. If unfamiliar, review the [2-minute YouTube video](https://youtu.be/fz4rbjRaiQM) for an overview and check out [CloudShell with VPC environment](https://docs.aws.amazon.com/cloudshell/latest/userguide/creating-vpc-environment.html) that we'll use to test nodes API from internal IP address space. + +Once ready, you can run the commands to deploy and test blueprints in the CloudShell. + +#### Cloning the Repository + +First, clone the repository and install the dependencies: + +``` +git clone https://github.com/aws-samples/aws-blockchain-node-runners.git +cd aws-blockchain-node-runners +npm install +``` + +Before proceeding, ensure you have the AWS CLI installed and configured. + +### Configuration + +1. Make sure you are in the root directory of the cloned repository. + +2. If you have deleted or don't have the default VPC, create default VPC + +``` +aws ec2 create-default-vpc +``` +> **NOTE:** *You may see the following error if the default VPC already exists: `An error occurred (DefaultVpcAlreadyExists) when calling the CreateDefaultVpc operation: A Default VPC already exists for this account in this region.`. That means you can just continue with the following steps.* + +3. Create your own copy of `.env` file and edit it to update with your AWS Account ID and Region: + +```bash +cd lib/bitcoin-core +cp ./sample-configs/.env-sample-bitcoin-mainnet .env +vim .env +``` + +4. Deploy common components such as IAM role: + +```bash +npx cdk deploy BitcoinCommonStack +``` + +The blueprint attaches a separate EBS volume (default 1 TB) to store the +blockchain data. During instance initialization this volume is automatically +formatted and mounted at `/home/bitcoin`, ensuring ample space for the +`datadir`. + +### Generating RPC Authentication + +To interact with the Bitcoin Core RPC endpoint within your isolated VPC environment, run the following command before deploying the Bitcoin Node via CDK: + +``` +# Make sure you are in aws-blockchain-node-runners/lib/bitcoin-core +node generateRPCAuth.js +``` + +For a deeper dive and an overview of credential rotation, see [RPC Authentication -- Deep Dive](#rpc-authentication----deep-dive). + + +### Deploying the Node + +To deploy a single node setup, use the following command: + +``` +npx cdk deploy SingleNodeBitcoinCoreStack --json --outputs-file single-node-deploy.json +``` + +For High Availability (HA) node deployment, use: + +``` +npx cdk deploy HABitcoinCoreNodeStack --json --outputs-file ha-nodes-deploy.json +``` + +### Deployment Architectures for Bitcoin Nodes + +#### Single Node Setup +![Single Node Deployment](./doc/assets/Bitcoin-Single-Node-Arch.png) + +- A **Bitcoin node** deployed in a **public subnet** continuously synchronizes with the Bitcoin network. +- Outbound peer-to-peer (P2P) communication flows through an **Internet Gateway (IGW)**. +- The node's security group permits incoming P2P connections on port 8333. +- The node's RPC methods can be accessed from within the VPC. +- The Bitcoin node sends various monitoring metrics to Amazon CloudWatch. + +#### High Availability (HA) Setup +![HA Node Deployment](./doc/assets/Bitcoin-HA-Nodes-Arch.png) + +- Deploying **multiple Bitcoin nodes** in an **Auto Scaling Group** enhances fault tolerance and availability. +- The nodes' RPC endpoints are exposed through an **Application Load Balancer (ALB)**. The ALB implements session persistence using a "stickiness cookie". This ensures that subsequent requests from the same client are consistently routed to the same node, maintaining session continuity. The stickiness duration is set to 90 minutes but can be configured for up to 7 days. Note: The Bitcoin Core nodes in the HA setup do not share state (e.g., wallet, mempool) +- HA nodes do not expose the RPC endpoint to the public internet. This endpoint can be accessed from within the VPC. + +--- + +### Accessing and Using bitcoin-cli on a Bitcoin Core Instance + +To interact with your Bitcoin Core instance, you'll need to use AWS Systems Manager, as direct SSH access is not available. + +Bitcoin Core supports cookie-based authentication by default, so interacting with the `bitcoin-cli` from the node itself does not require credentials. + +From your CloudShell terminal, run the following command to connect to your node via Systems Manager: + +``` +export INSTANCE_ID=$(jq -r '.SingleNodeBitcoinCoreStack.BitcoinNodeInstanceId' single-node-deploy.json) +echo "INSTANCE_ID="$INSTANCE_ID +aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION +``` + +**Note**: You can alternatively connect to your node via Systems Manager in the AWS Console with the following steps: + - Open the AWS Console and navigate to EC2 Instances. + - Locate and select the instance named `SingleNodeBitcoinCoreStack/BitcoinSingleNode`. + - Click the "Connect" button. + - Choose "Session Manager" from the connection options. + - Select "Connect" to establish a session. + +**Execute an RPC Call:** +Once connected, you can interact with the Bitcoin Core node using Docker commands. + +To test the RPC interface, use the following command: + +``` +sudo docker exec -it bitcoind bitcoin-cli getblockchaininfo +``` + + This command executes the `getblockchaininfo` RPC method, which returns current state information about the blockchain. + +**Interpreting Results:** + - The output will provide detailed information about the current state of the blockchain, including the current block height, difficulty, and other relevant data. + - You can use similar commands to execute other RPC methods supported by Bitcoin Core. + +--- +### Secure RPC Access with AWS Secrets Manager + +For a client to securely interact with the Bitcoin Core RPC endpoint from a subnet within your VPC environment, AWS Secrets Manager is leveraged for credential storage and retrieval. + +#### Retrieving Credentials +First, retrieve the RPC credentials from AWS Secrets Manager in your CloudShell tab: + +``` +export BTC_RPC_AUTH=$(aws secretsmanager get-secret-value --secret-id bitcoin_rpc_credentials --query SecretString --output text) +echo "BTC_RPC_AUTH=$BTC_RPC_AUTH" +``` + +#### Single node RPC Call using credentials +To make an RPC call to a single Bitcoin node, run the following command to retrieve the private IP address of your Bitcoin node: + +``` +export BITCOIN_NODE_IP=$(jq -r '.SingleNodeBitcoinCoreStack.BitcoinNodePrivateIP' single-node-deploy.json) +echo "BITCOIN_NODE_IP=$BITCOIN_NODE_IP" +``` +Copy output from the last `echo` command with `BITCOIN_NODE_IP=` and open [CloudShell tab with VPC environment](https://docs.aws.amazon.com/cloudshell/latest/userguide/creating-vpc-environment.html) to access internal IP address space. Paste `BITCOIN_NODE_IP=` into the new CloudShell tab. + +Additionally, copy the output from the first `echo` command with `BTC_RPC_AUTH=` into the CloudShell VPC environment. + +Then query the node: + +``` +curl --user "$BTC_RPC_AUTH" \ + --data-binary '{"jsonrpc": "1.0", "id": "curltest", "method": "getblockchaininfo", "params": []}' \ + -H 'content-type: text/plain;' http://$BITCOIN_NODE_IP:8332/ +``` + +#### High Availability (HA) RPC Call using credentials + +Use the following command from your CloudShell terminal to retrieve your load balancer's DNS name: + +``` +export LOAD_BALANCER_DNS=$(jq -r '.HABitcoinCoreNodeStack.LoadBalancerDNS' ha-nodes-deploy.json) +echo LOAD_BALANCER_DNS=$LOAD_BALANCER_DNS +``` +Copy output from the last `echo` command with `RPC_ABL_URL=` and open [CloudShell tab with VPC environment](https://docs.aws.amazon.com/cloudshell/latest/userguide/creating-vpc-environment.html) to access internal IP address space. Paste `RPC_ABL_URL=` into the new CloudShell tab. + +Note: Make sure that you pasted `BTC_RPC_AUTH=` into the CloudShell VPC environment as well. + + Execute the following command to make an RPC request to your HA node setup: + +``` +curl --user "$BTC_RPC_AUTH" \ + --data-binary '{"jsonrpc": "1.0", "id": "curltest", "method": "getblockchaininfo", "params": []}' \ + -H 'content-type: text/plain;' \ + $LOAD_BALANCER_DNS +``` + +--- + + +### **Bitcoin Core: Creating an Encrypted Wallet for Payments** + +This guide covers how to create an encrypted Bitcoin Core wallet specifically designed for receiving and managing payments in a secure and efficient way. + +Note: Make sure that you run the following commands after accessing the node via Systems Manager. + +--- + +#### **1. Create an Encrypted Payment Wallet** + +To create a wallet specifically for handling payments, use the following command: + +``` +sudo docker exec -it bitcoind bitcoin-cli createwallet "payments" false false "my_secure_passphrase" +``` + +- **payments:** The wallet name, indicating its purpose. +- **passphrase:** A secure, memorable phrase to protect your funds. + +##### **Why Encrypt?** +- Protects against unauthorized access. +- Ensures funds are safe even if the server is compromised. + +--- + +#### **2. Generate a Receiving Address** + +To receive payments, generate a new address. You do not need to unlock the wallet for this step: + +``` +sudo docker exec -it bitcoind bitcoin-cli -rpcwallet="payments" getnewaddress "customer1" "bech32" +``` + +- **customer1:** Label to identify payments from this customer. +- **bech32:** Generates a SegWit address for lower transaction fees. + +**Example Output:** +``` +bc1qxyzabc123... (Bech32 address) +``` + +--- + +#### **3. Monitor Incoming Payments** + +To check the balance and verify received payments: + +``` +sudo docker exec -it bitcoind bitcoin-cli -rpcwallet="payments" getbalance +``` + +- Displays the total balance held in the wallet. + +To view detailed transactions: + +``` +sudo docker exec -it bitcoind bitcoin-cli -rpcwallet="payments" listtransactions +``` + +--- + +#### **4. Sending Payments (Requires Unlocking)** + +When making a payout or transferring funds, you need to unlock the wallet: + +``` +sudo docker exec -it bitcoind bitcoin-cli -rpcwallet="payments" walletpassphrase "my_secure_passphrase" 600 +``` + +- Unlocks the wallet for **600 seconds (10 minutes)**. + +#### **Send Bitcoin to a specified address:** + +``` +sudo docker exec -it bitcoind bitcoin-cli -rpcwallet="payments" sendtoaddress "bc1qrecipientaddress" 0.01 "Payment for service" +``` + +- Sends **0.01 BTC** with an optional label for record-keeping. + + +#### **5. Lock the Wallet After Use** + +For enhanced security, immediately lock the wallet after transactions: + +``` +sudo docker exec -it bitcoind bitcoin-cli -rpcwallet="payments" walletlock +``` + + + +#### **6. Backup the Wallet** + +To protect your payment data, back up the encrypted wallet regularly: + +``` +sudo docker exec -it bitcoind bitcoin-cli -rpcwallet="payments" backupwallet "/path/to/backup/payments.dat" +``` + + +#### **Security Tips for Payment Wallets** +- Use strong passphrases and store them securely offline. +- Regularly backup your wallet after creating new addresses or receiving payments. +- Consider setting up automated wallet backups to ensure data integrity. + +--- +### RPC Authentication -- Deep Dive + +The `generateRPCAuth.js` script is responsible for generating secure authentication credentials for your Bitcoin node. This script creates a randomly generated **username** and **password** along with a **salt**. The password and salt are then combined and hashed using the **SHA256** algorithm to produce a secure **hash**. This hash is combined with the username to generate the final **rpcauth** parameter that is appended to the `bitcoin.conf` file. + +The final `rpcauth` line in `bitcoin.conf` looks like this: + +``` +rpcauth=user_258:c220c5f38690bf880f0dd177547e55f7$77c6ec2dd90e792d60450b01a84cc8c2563a7fb1d0fbd73de49be818fde4b407 +``` + +- The **rpcauth** part consists of a **username**, **salt**, and a **hashed password**, providing robust protection in the case that your `bitcoin.conf` is accessed by an unauthorized entity. +- The randomly generated **username** and **password** are securely stored in **AWS Secrets Manager**. + +By using this script, it ensures that your node has unique and secure credentials. + +### Rotating RPC Secrets + +To maintain security, rotate RPC credentials periodically using the `generateRPCAuth.js` script: + +``` +node generateRPCAuth.js +``` + +This will update the value of your credentials in Secrets Manager. + +**Replacing the Credentials and Restarting the Node to Apply Updates** + +- Replace the old `rpcauth` value from the `bitcoin.conf` file with the new one. Make sure that you change the placeholder value for `[new rpcauth string with escape char]` (this is printed to the terminal after running the `generateRPCAuth` script): + + ``` + sudo docker exec -it bitcoind sh -c "sed -i 's/^rpcauth=.*/rpcauth=[new rpcauth string with escape char]/' /root/.bitcoin/bitcoin.conf" + ``` +- Restart the Bitcoin node to apply changes: + ``` + sudo docker restart bitcoind + ``` + +#### Verifying the Credential Rotation + +Make an RPC call to ensure the new credentials are active: + +``` +curl --user "$BTC_RPC_AUTH" \ + --data-binary '{"jsonrpc": "1.0", "id": "curltest", "method": "getblockchaininfo", "params": []}' \ + -H 'content-type: text/plain;' http://:8332/ +``` + +--- + +### Monitoring and Troubleshooting + +Keep your node healthy by monitoring logs and configurations. + +These can be run after accessing the node via Systems Manager: + +``` +export INSTANCE_ID=$(jq -r '.SingleNodeBitcoinCoreStack.BitcoinNodeInstanceId' single-node-deploy.json) +echo "INSTANCE_ID="$INSTANCE_ID +aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION +``` + +- Check recent Bitcoin logs: + ``` + sudo docker logs -f --tail 100 bitcoind + ``` + +- Check first 100 Bitcoin logs: + ``` + sudo docker logs bitcoind | head -n 100 + ``` + +- View the configuration file: + ``` + sudo docker exec -it bitcoind cat /root/.bitcoin/bitcoin.conf + ``` +- View user data logs: + ``` + sudo cat /var/log/cloud-init-output.log + ``` + + +--- + +### Additional Tips and Best Practices + +- Regularly rotate secrets and always remove old `rpcauth` entries before restarting the node. +- Use **CloudWatch** to monitor node performance and detect issues promptly. + +--- + +### Cleaning up +To destroy the single node and HA configurations, you can run the following commands: + +``` +#Delete Single Node Infra +cdk destroy SingleNodeBitcoinCoreStack + +#Delete HA Infra +cdk destroy HABitcoinCoreNodeStack +``` + + +--- + +### Conclusion + +Deploying and managing a Bitcoin node on AWS requires careful configuration to ensure security, cost efficiency, and high availability. By following the best practices outlined in this guide, you can maintain a robust and secure node while minimizing costs. Stay proactive with monitoring and regularly update credentials to keep your node running smoothly. diff --git a/lib/bitcoin-core/app.ts b/lib/bitcoin-core/app.ts new file mode 100644 index 00000000..1f63f80b --- /dev/null +++ b/lib/bitcoin-core/app.ts @@ -0,0 +1,31 @@ +#!/usr/bin/env node +import 'dotenv/config'; +import { App, Aspects } from 'aws-cdk-lib'; +import { AwsSolutionsChecks } from 'cdk-nag'; +import { BitcoinCommonStack } from './lib/common-infra'; +import { SingleNodeBitcoinCoreStack } from './lib/single-node-stack'; +import { HABitcoinCoreNodeStack } from './lib/ha-node-stack'; +import * as config from './lib/config/bitcoinConfig'; + +const app = new App(); + +Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true })); + +const env = { + account: process.env.AWS_ACCOUNT_ID, + region: process.env.AWS_REGION, +}; + +const commonStack = new BitcoinCommonStack(app, 'BitcoinCommonStack', { env }); +new SingleNodeBitcoinCoreStack(app, 'SingleNodeBitcoinCoreStack', { + env, + instanceRole: commonStack.instanceRole, + ...config.baseNodeConfig, +}); + +new HABitcoinCoreNodeStack(app, 'HABitcoinCoreNodeStack', { + env, + instanceRole: commonStack.instanceRole, + ...config.baseNodeConfig, + ...config.haNodeConfig, +}); diff --git a/lib/bitcoin-core/cdk.json b/lib/bitcoin-core/cdk.json new file mode 100644 index 00000000..7714e8c2 --- /dev/null +++ b/lib/bitcoin-core/cdk.json @@ -0,0 +1,57 @@ +{ + "app": "npx ts-node --prefer-ts-exts app.ts", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-iam:standardizedServicePrincipals": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true + } +} diff --git a/lib/bitcoin-core/doc/assets/Bitcoin-HA-Nodes-Arch.png b/lib/bitcoin-core/doc/assets/Bitcoin-HA-Nodes-Arch.png new file mode 100644 index 00000000..0aa8a7df Binary files /dev/null and b/lib/bitcoin-core/doc/assets/Bitcoin-HA-Nodes-Arch.png differ diff --git a/lib/bitcoin-core/doc/assets/Bitcoin-Single-Node-Arch.png b/lib/bitcoin-core/doc/assets/Bitcoin-Single-Node-Arch.png new file mode 100644 index 00000000..6c2f9d97 Binary files /dev/null and b/lib/bitcoin-core/doc/assets/Bitcoin-Single-Node-Arch.png differ diff --git a/lib/bitcoin-core/generateRPCAuth.js b/lib/bitcoin-core/generateRPCAuth.js new file mode 100644 index 00000000..c3d59f50 --- /dev/null +++ b/lib/bitcoin-core/generateRPCAuth.js @@ -0,0 +1,100 @@ +const crypto = require('crypto'); +const base64url = require('base64url'); +const fs = require('fs'); +const { SecretsManagerClient, CreateSecretCommand, PutSecretValueCommand } = require('@aws-sdk/client-secrets-manager'); + +// Set up AWS SDK client +const client = new SecretsManagerClient({ region: 'us-east-1' }); // Change region if needed + +// Create size byte hex salt +function genSalt(size = 16) { + const buffer = crypto.randomBytes(size); + return buffer.toString('hex'); +} + +// Create 32 byte b64 password +function genPass(size = 32) { + const buffer = crypto.randomBytes(size); + return base64url.fromBase64(buffer.toString('base64')); +} + +function genUser() { + return 'user_' + Math.round(Math.random() * 1000); +} + +function genHash(password, salt) { + const hash = crypto + .createHmac('sha256', salt) + .update(password) + .digest('hex'); + return hash; +} + +function genRpcAuth(username = genUser(), password = genPass(), salt = genSalt()) { + const hash = genHash(password, salt); + return { username, password, salt, hash }; +} + +function writeRpcAuthToConf(rpcauthStr) { + const confPath = 'lib/bitcoin.conf'; + try { + fs.writeFileSync(confPath, rpcauthStr + '\n', { flag: 'a' }); + console.log(`Successfully wrote to ${confPath}`); + } catch (error) { + console.error(`Error writing to ${confPath}:`, error); + } +} + +async function storeCredentialsInAWS(username, password) { + const secretName = 'bitcoin_rpc_credentials'; + const secretValue = `${username}:${password}`; + + try { + const createCommand = new CreateSecretCommand({ + Name: secretName, + SecretString: secretValue, + }); + await client.send(createCommand); + console.log(`Successfully stored credentials in AWS Secrets Manager: ${secretName}`); + } catch (error) { + if (error.name === 'ResourceExistsException') { + const updateCommand = new PutSecretValueCommand({ + SecretId: secretName, + SecretString: secretValue, + }); + await client.send(updateCommand); + console.log(`Successfully updated existing secret in AWS Secrets Manager: ${secretName}`); + } else { + console.error(`Error storing credentials in AWS Secrets Manager:`, error); + } + } +} + +async function genRpcAuthStr(username, password, salt) { + const rpcauth = genRpcAuth(username, password, salt); + const str = `rpcauth=${rpcauth.username}:${rpcauth.salt}$${rpcauth.hash}`; + const strEscapeCharacter = `${rpcauth.username}:${rpcauth.salt}\\$${rpcauth.hash}`; + console.log(`Username: ${rpcauth.username}`); + console.log("Password generated securely and stored in Secrets Manager"); + console.log(`rpcauth string with escape character: ${strEscapeCharacter}`); // Print the rpcauth string + + // Write to bitcoin.conf + writeRpcAuthToConf(str); + + // Store in AWS Secrets Manager + await storeCredentialsInAWS(rpcauth.username, rpcauth.password); + + return str; +} + +// Example usage +genRpcAuthStr(); + +module.exports = { + genSalt, + genPass, + genUser, + genHash, + genRpcAuth, + genRpcAuthStr, +}; diff --git a/lib/bitcoin-core/jest.config.js b/lib/bitcoin-core/jest.config.js new file mode 100644 index 00000000..08263b89 --- /dev/null +++ b/lib/bitcoin-core/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + testEnvironment: 'node', + roots: ['/test'], + testMatch: ['**/*.test.ts'], + transform: { + '^.+\\.tsx?$': 'ts-jest' + } +}; diff --git a/lib/bitcoin-core/lib/assets/bitcoin-setup.sh b/lib/bitcoin-core/lib/assets/bitcoin-setup.sh new file mode 100755 index 00000000..f8704cd9 --- /dev/null +++ b/lib/bitcoin-core/lib/assets/bitcoin-setup.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# This script is used to set up a mainnet Bitcoin Core node on an Amazon Linux 2 instance. +set -euo pipefail + +# The stack passes lifecycle hook information when used in an Auto +# Scaling Group. If no lifecycle hook name is supplied (single node +# deployment), default to "none" and use CloudFormation signaling. +LIFECYCLE_HOOK_NAME=${LIFECYCLE_HOOK_NAME:-none} +AUTOSCALING_GROUP_NAME=${AUTOSCALING_GROUP_NAME:-none} + +# Ensure the data volume is mounted before proceeding +until mountpoint -q /home/bitcoin; do + echo "Waiting for /home/bitcoin to be mounted..." + sleep 2 +done + +yum update -y +amazon-linux-extras install docker -y +service docker start +systemctl enable docker + +# Create bitcoin user with specific UID:GID to match the container's expected values +if id -u bitcoin > /dev/null 2>&1; then + # User exists, update to correct UID:GID + usermod -u 101 bitcoin + groupmod -g 101 bitcoin +else + # Create user with specific UID:GID + groupadd -g 101 bitcoin + useradd -u 101 -g 101 -m -s /bin/bash bitcoin +fi + +# Create the bitcoin data directory structure on the mounted EBS volume +mkdir -p /home/bitcoin/.bitcoin +echo "${BITCOIN_CONF}" > /home/bitcoin/.bitcoin/bitcoin.conf + +# Set proper permissions for the Bitcoin configuration +chown -R bitcoin:bitcoin /home/bitcoin +chmod -R 755 /home/bitcoin + +# Run Bitcoin Core in Docker with proper volume mapping +# Modified to ensure data is stored on the EBS volume +docker run -d --name bitcoind \ + -v /home/bitcoin/.bitcoin:/home/bitcoin/.bitcoin \ + -p 8333:8333 \ + -p 8332:8332 \ + --restart unless-stopped \ + bitcoin/bitcoin:latest \ + bitcoind -datadir=/home/bitcoin/.bitcoin + +# Signal completion depending on deployment type +if [[ "$LIFECYCLE_HOOK_NAME" != "none" ]]; then + echo "Signaling ASG lifecycle hook to complete" + TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") + INSTANCE_ID=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/instance-id) + aws autoscaling complete-lifecycle-action --lifecycle-action-result CONTINUE --instance-id "$INSTANCE_ID" --lifecycle-hook-name "$LIFECYCLE_HOOK_NAME" --auto-scaling-group-name "$AUTOSCALING_GROUP_NAME" --region "$AWS_REGION" +else + if ! command -v cfn-signal &> /dev/null; then + yum install -y aws-cfn-bootstrap + fi + cfn-signal --stack "$STACK_NAME" --resource "$RESOURCE_ID" --region "$AWS_REGION" +fi diff --git a/lib/bitcoin-core/lib/assets/blockheight-cron.sh b/lib/bitcoin-core/lib/assets/blockheight-cron.sh new file mode 100755 index 00000000..fcc7f192 --- /dev/null +++ b/lib/bitcoin-core/lib/assets/blockheight-cron.sh @@ -0,0 +1,4 @@ +#!/bin/bash +# This script is used to set up a cron job to send the Bitcoin block height to Amazon CloudWatch every 5 minutes. +REGION=${AWS_REGION} +(crontab -l 2>/dev/null; echo "*/5 * * * * sudo /usr/bin/docker exec bitcoind bitcoin-cli getblockcount | xargs -I {} sudo /usr/bin/aws cloudwatch put-metric-data --metric-name BlockHeight --namespace Bitcoin --unit Count --value {} --region $REGION") | crontab - diff --git a/lib/bitcoin-core/lib/assets/cloudwatch-setup.sh b/lib/bitcoin-core/lib/assets/cloudwatch-setup.sh new file mode 100755 index 00000000..831788bb --- /dev/null +++ b/lib/bitcoin-core/lib/assets/cloudwatch-setup.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# This script is used to set up the Amazon CloudWatch agent on an Amazon Linux 2 instance. + +yum install -y amazon-cloudwatch-agent +mkdir -p /opt/aws/amazon-cloudwatch-agent/etc +cat < /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json +{ + "metrics": { + "metrics_collected": { + "disk": { + "measurement": ["used_percent", "inodes_free"], + "resources": ["*"], + "ignore_file_system_types": ["sysfs", "devtmpfs"] + }, + "mem": { + "measurement": ["mem_used_percent"] + }, + "cpu": { + "measurement": ["cpu_usage_idle", "cpu_usage_user", "cpu_usage_system"] + }, + "net": { + "measurement": ["net_bytes_sent", "net_bytes_recv"], + "resources": ["eth0"] + } + } + } +} +EOF + +/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -c file:/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json -s diff --git a/lib/bitcoin-core/lib/assets/storage-setup.sh b/lib/bitcoin-core/lib/assets/storage-setup.sh new file mode 100755 index 00000000..ce516462 --- /dev/null +++ b/lib/bitcoin-core/lib/assets/storage-setup.sh @@ -0,0 +1,171 @@ +#!/bin/bash +set -euo pipefail + +make_fs () { + # If file system = to ext4 use mkfs.ext4, if xfs use mkfs.xfs + if [ -z "$1" ]; then + echo "Error: No file system type provided." + echo "Usage: make_fs " + exit 1 + fi + + if [ -z "$2" ]; then + echo "Error: No target volume ID provided." + echo "Usage: make_fs " + exit 1 + fi + + local file_system=$1 + local volume_id=$2 + if [ "$file_system" == "ext4" ]; then + mkfs -t ext4 "$volume_id" + return "$?" + else + mkfs.xfs -f "$volume_id" + return "$?" + fi +} + +# We need an nvme disk that is not mounted and not partitioned +get_all_empty_nvme_disks () { + local all_not_mounted_nvme_disks + local all_mounted_nvme_partitions + local unmounted_nvme_disks=() + local sorted_unmounted_nvme_disks + + #The disk will only be mounted when the nvme disk is larger than 100GB to avoid storing blockchain node data directly on the root EBS disk (which is 46GB by default) + all_not_mounted_nvme_disks=$(lsblk -lnb | awk '{if ($7 == "" && $4 > 107374182400) {print $1}}' | grep nvme) + all_mounted_nvme_partitions=$(mount | awk '{print $1}' | grep /dev/nvme) + for disk in ${all_not_mounted_nvme_disks[*]}; do + if [[ ! "${all_mounted_nvme_partitions[*]}" =~ $disk ]]; then + unmounted_nvme_disks+=("$disk") + fi + done + # Sort the array + sorted_unmounted_nvme_disks=($(printf '%s\n' "${unmounted_nvme_disks[*]}" | sort)) + echo "${sorted_unmounted_nvme_disks[*]}" +} + +get_next_empty_nvme_disk () { + local sorted_unmounted_nvme_disks + sorted_unmounted_nvme_disks=($(get_all_empty_nvme_disks)) + # Return the first unmounted nvme disk + echo "/dev/${sorted_unmounted_nvme_disks[0]}" +} + +# Add input as command line parameters for name of the directory to mount +if [ -n "$1" ]; then + DIR_NAME=$1 +else + echo "Error: No data file system mount path is provided." + echo "Usage: instance/storage/setup.sh " + echo "Default file system type is ext4" + echo "If you skip , script will try to use the first unformatted volume ID." + echo "Usage example: instance/storage/setup.sh /data ext4 300000000000000" + exit 1 +fi + +# Create the directory if it doesn't exist +if [ ! -d "$DIR_NAME" ]; then + echo "Creating directory $DIR_NAME" + mkdir -p "$DIR_NAME" +fi + +# Case input for $2 between ext4 and xfs, use ext4 as default +case $2 in + ext4) + echo "File system set to ext4" + FILE_SYSTEM="ext4" + FS_CONFIG="defaults" + ;; + xfs) + echo "File system set to xfs" + FILE_SYSTEM="xfs" + FS_CONFIG="noatime,nodiratime,nodiscard" # See more: https://cdrdv2-public.intel.com/686417/rocksdb-benchmark-tuning-guide-on-xeon.pdf + ;; + *) + echo "File system set to ext4" + FILE_SYSTEM="ext4" + FS_CONFIG="defaults" + ;; +esac + +if [ -n "$3" ]; then + VOLUME_SIZE=$3 +else + echo "The size of volume for $DIR_NAME is not specified. Will try to guess volume ID." +fi + + echo "Checking if $DIR_NAME is mounted, and dont do anything if it is" + if [ $(df --output=target | grep -c "$DIR_NAME") -lt 1 ]; then + + if [ -n "$VOLUME_SIZE" ]; then + VOLUME_ID=/dev/$(lsblk -lnb | awk -v VOLUME_SIZE_BYTES="$VOLUME_SIZE" '{if ($4== VOLUME_SIZE_BYTES) {print $1}}') + echo "Data volume size defined, use respective volume id: $VOLUME_ID" + else + VOLUME_ID=$(get_next_empty_nvme_disk) + echo "Data volume size undefined, trying volume id: $VOLUME_ID" + fi + + attempts=0 + until [ -n "$VOLUME_ID" ] || [ $attempts -ge 30 ]; do + echo "Waiting for data volume to appear..." + sleep 5 + if [ -n "$VOLUME_SIZE" ]; then + VOLUME_ID=/dev/$(lsblk -lnb | awk -v VOLUME_SIZE_BYTES="$VOLUME_SIZE" '{if ($4== VOLUME_SIZE_BYTES) {print $1}}') + else + VOLUME_ID=$(get_next_empty_nvme_disk) + fi + attempts=$((attempts+1)) + done + + if [ -z "$VOLUME_ID" ]; then + echo "Error: Could not find a suitable volume to mount. Listing available disks:" + lsblk -lnb + exit 1 + fi + + echo "Formatting volume $VOLUME_ID with $FILE_SYSTEM" + make_fs $FILE_SYSTEM "$VOLUME_ID" + if [ $? -ne 0 ]; then + echo "Error: Failed to format the volume. Exiting." + exit 1 + fi + + sleep 10 + VOLUME_UUID=$(lsblk -fn -o UUID "$VOLUME_ID") + if [ -z "$VOLUME_UUID" ]; then + echo "Error: Could not get UUID for volume $VOLUME_ID. Exiting." + exit 1 + fi + + VOLUME_FSTAB_CONF="UUID=$VOLUME_UUID $DIR_NAME $FILE_SYSTEM $FS_CONFIG 0 2" + echo "VOLUME_ID=$VOLUME_ID" + echo "VOLUME_UUID=$VOLUME_UUID" + echo "VOLUME_FSTAB_CONF=$VOLUME_FSTAB_CONF" + + # Check if data disc is already in fstab and replace the line if it is with the new disc UUID + echo "Checking fstab for volume $DIR_NAME" + if [ $(grep -c "$DIR_NAME" /etc/fstab) -gt 0 ]; then + SED_REPLACEMENT_STRING="$(grep -n "$DIR_NAME" /etc/fstab | cut -d: -f1)s#.*#$VOLUME_FSTAB_CONF#" + # if file exists, delete it + if [ -f /etc/fstab.bak ]; then + rm /etc/fstab.bak + fi + cp /etc/fstab /etc/fstab.bak + sed -i "$SED_REPLACEMENT_STRING" /etc/fstab + else + echo "$VOLUME_FSTAB_CONF" | tee -a /etc/fstab + fi + + echo "Mounting volume with mount -a" + mount -a + if [ $? -ne 0 ]; then + echo "Error: Failed to mount the volume. Exiting." + exit 1 + fi + + echo "Volume mounted successfully at $DIR_NAME" + else + echo "$DIR_NAME volume is mounted, nothing changed" + fi diff --git a/lib/bitcoin-core/lib/bitcoin.conf b/lib/bitcoin-core/lib/bitcoin.conf new file mode 100644 index 00000000..dff45892 --- /dev/null +++ b/lib/bitcoin-core/lib/bitcoin.conf @@ -0,0 +1,4 @@ +server=1 +rpcallowip=0.0.0.0/0 +txindex=1 +rpcbind=0.0.0.0:8332 diff --git a/lib/bitcoin-core/lib/common-infra.ts b/lib/bitcoin-core/lib/common-infra.ts new file mode 100644 index 00000000..23d61a04 --- /dev/null +++ b/lib/bitcoin-core/lib/common-infra.ts @@ -0,0 +1,37 @@ +import * as cdk from "aws-cdk-lib"; +import * as iam from "aws-cdk-lib/aws-iam"; +import { NagSuppressions } from "cdk-nag"; +import { Construct } from "constructs"; + +export class BitcoinCommonStack extends cdk.Stack { + public readonly instanceRole: iam.Role; + + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + this.instanceRole = new iam.Role(this, "BitcoinNodeRole", { + assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore"), + iam.ManagedPolicy.fromAwsManagedPolicyName("CloudWatchAgentServerPolicy"), + ], + }); + + new cdk.CfnOutput(this, "InstanceRoleArn", { + value: this.instanceRole.roleArn, + exportName: "BitcoinNodeInstanceRoleArn", + }); + + // cdk-nag suppressions + NagSuppressions.addResourceSuppressions(this.instanceRole, [ + { + id: "AwsSolutions-IAM4", + reason: "AmazonSSMManagedInstanceCore and CloudWatchAgentServerPolicy are sufficient for this use case.", + }, + { + id: "AwsSolutions-IAM5", + reason: "Managed policies and wildcard usage are acceptable for this limited-scope Bitcoin node role.", + }, + ]); + } +} diff --git a/lib/bitcoin-core/lib/config/bitcoinConfig.interface.ts b/lib/bitcoin-core/lib/config/bitcoinConfig.interface.ts new file mode 100644 index 00000000..5a3eeca7 --- /dev/null +++ b/lib/bitcoin-core/lib/config/bitcoinConfig.interface.ts @@ -0,0 +1,15 @@ +import * as configTypes from "../../../constructs/config.interface"; + +export interface BitcoinDataVolumeConfig extends configTypes.DataVolumeConfig {} + +export interface BitcoinBaseNodeConfig extends configTypes.BaseNodeConfig { + dataVolume: BitcoinDataVolumeConfig; +} + +export interface BitcoinHAConfig { + albHealthCheckGracePeriodMin: number; + heartBeatDelayMin: number; + numberOfNodes: number; +} + +export interface BitcoinBaseConfig extends configTypes.BaseConfig {} diff --git a/lib/bitcoin-core/lib/config/bitcoinConfig.ts b/lib/bitcoin-core/lib/config/bitcoinConfig.ts new file mode 100644 index 00000000..93572538 --- /dev/null +++ b/lib/bitcoin-core/lib/config/bitcoinConfig.ts @@ -0,0 +1,40 @@ +import * as ec2 from "aws-cdk-lib/aws-ec2"; +import * as configTypes from "./bitcoinConfig.interface"; +import * as constants from "../../../constructs/constants"; + +const parseDataVolumeType = (dataVolumeType: string) => { + switch (dataVolumeType) { + case "gp3": + return ec2.EbsDeviceVolumeType.GP3; + case "io2": + return ec2.EbsDeviceVolumeType.IO2; + case "io1": + return ec2.EbsDeviceVolumeType.IO1; + case "instance-store": + return constants.InstanceStoreageDeviceVolumeType; + default: + return ec2.EbsDeviceVolumeType.GP3; + } +}; + +export const baseConfig: configTypes.BitcoinBaseConfig = { + accountId: process.env.AWS_ACCOUNT_ID || "xxxxxxxxxxx", + region: process.env.AWS_REGION || "us-east-1", +}; + +export const baseNodeConfig: configTypes.BitcoinBaseNodeConfig = { + instanceType: new ec2.InstanceType(process.env.BTC_INSTANCE_TYPE ? process.env.BTC_INSTANCE_TYPE : "t3a.large"), + instanceCpuType: process.env.CPU_ARCHITECTURE?.toUpperCase() === "ARM64" ? ec2.AmazonLinuxCpuType.ARM_64 : ec2.AmazonLinuxCpuType.X86_64, + dataVolume: { + sizeGiB: process.env.EBS_VOLUME_SIZE ? parseInt(process.env.EBS_VOLUME_SIZE) : 1000, + type: parseDataVolumeType(process.env.EBS_VOLUME_TYPE?.toLowerCase() || "gp3"), + iops: process.env.GP3_IOPS ? parseInt(process.env.GP3_IOPS) : 3000, + throughput: process.env.GP3_THROUGHPUT ? parseInt(process.env.GP3_THROUGHPUT) : 125, + }, +}; + +export const haNodeConfig: configTypes.BitcoinHAConfig = { + albHealthCheckGracePeriodMin: process.env.ALB_HEALTHCHECK_GRACE_PERIOD_MIN ? parseInt(process.env.ALB_HEALTHCHECK_GRACE_PERIOD_MIN) : 10, + heartBeatDelayMin: process.env.HEARTBEAT_DELAY_MIN ? parseInt(process.env.HEARTBEAT_DELAY_MIN) : 40, + numberOfNodes: process.env.ASG_DESIRED_CAPACITY ? parseInt(process.env.ASG_DESIRED_CAPACITY) : 2, +}; diff --git a/lib/bitcoin-core/lib/constructs/bitcoin-mainnet-security-group.ts b/lib/bitcoin-core/lib/constructs/bitcoin-mainnet-security-group.ts new file mode 100644 index 00000000..b9cec658 --- /dev/null +++ b/lib/bitcoin-core/lib/constructs/bitcoin-mainnet-security-group.ts @@ -0,0 +1,20 @@ +import { Construct } from 'constructs'; +import * as ec2 from 'aws-cdk-lib/aws-ec2'; + +export class BitcoinSecurityGroup extends Construct { + public readonly securityGroup: ec2.SecurityGroup; + + constructor(scope: Construct, id: string, vpc: ec2.IVpc) { + super(scope, id); + + const sg = new ec2.SecurityGroup(this, 'BitcoinSG', { + vpc, + allowAllOutbound: true, + }); + + sg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(8333), 'Bitcoin P2P'); + sg.addIngressRule(ec2.Peer.ipv4(vpc.vpcCidrBlock), ec2.Port.tcp(8332), 'Bitcoin RPC from VPC'); + + this.securityGroup = sg; + } +} diff --git a/lib/bitcoin-core/lib/ha-node-stack.ts b/lib/bitcoin-core/lib/ha-node-stack.ts new file mode 100644 index 00000000..15f3d224 --- /dev/null +++ b/lib/bitcoin-core/lib/ha-node-stack.ts @@ -0,0 +1,110 @@ +import * as cdk from "aws-cdk-lib"; +import * as cdkConstructs from "constructs"; +import * as ec2 from "aws-cdk-lib/aws-ec2"; +import * as iam from "aws-cdk-lib/aws-iam"; +import * as fs from "fs"; +import * as path from "path"; +import { NagSuppressions } from "cdk-nag"; +import { BitcoinSecurityGroup } from "./constructs/bitcoin-mainnet-security-group"; +import { HANodesConstruct } from "../../constructs/ha-rpc-nodes-with-alb"; +import * as configTypes from "./config/bitcoinConfig.interface"; + +export interface BitcoinHANodesStackProps extends cdk.StackProps, configTypes.BitcoinBaseNodeConfig, configTypes.BitcoinHAConfig { + instanceRole: iam.IRole; +} + +export class HABitcoinCoreNodeStack extends cdk.Stack { + constructor(scope: cdkConstructs.Construct, id: string, props: BitcoinHANodesStackProps) { + super(scope, id, props); + + const REGION = cdk.Stack.of(this).region; + const STACK_NAME = cdk.Stack.of(this).stackName; + const lifecycleHookName = STACK_NAME; + const autoScalingGroupName = STACK_NAME; + + const { instanceType, instanceCpuType, dataVolume, numberOfNodes, albHealthCheckGracePeriodMin, heartBeatDelayMin, instanceRole } = props as BitcoinHANodesStackProps & { instanceRole: iam.IRole }; + + const vpc = ec2.Vpc.fromLookup(this, "vpc", { isDefault: true }); + + const instanceSG = new BitcoinSecurityGroup(this, "bitcoin-sg", vpc); + + const bitcoinSetup = fs.readFileSync(path.join(__dirname, "assets", "bitcoin-setup.sh"), "utf8"); + const storageSetup = fs.readFileSync(path.join(__dirname, "assets", "storage-setup.sh"), "utf8"); + const bitcoinConfPath = path.join(__dirname, "bitcoin.conf"); + const bitcoinConfContent = fs.readFileSync(bitcoinConfPath, "utf8"); + + // Calculate the volume size in bytes for the storage setup script + const dataVolumeSizeBytes = dataVolume.sizeGiB * 1073741824; // GiB to bytes conversion + + const userData = [ + "#!/bin/bash", + "set -euo pipefail", + `export AWS_REGION='${REGION}'`, + `export STACK_NAME='${STACK_NAME}'`, + `export RESOURCE_ID='none'`, + `export LIFECYCLE_HOOK_NAME='${lifecycleHookName}'`, + `export AUTOSCALING_GROUP_NAME='${autoScalingGroupName}'`, + `export BITCOIN_CONF='${bitcoinConfContent}'`, + "cat <<'EOF' > /opt/storage-setup.sh", + storageSetup, + "EOF", + "chmod +x /opt/storage-setup.sh", + // Create the bitcoin home directory first + "mkdir -p /home/bitcoin", + // Run the storage setup script with the volume size in bytes + `/opt/storage-setup.sh /home/bitcoin ext4 ${dataVolumeSizeBytes}`, + bitcoinSetup, + ].join("\n"); + + const rpcNodes = new HANodesConstruct(this, "rpc-nodes", { + instanceType, + dataVolumes: [dataVolume], + machineImage: ec2.MachineImage.latestAmazonLinux2({ cpuType: instanceCpuType }), + role: instanceRole, + vpc, + securityGroup: instanceSG.securityGroup, + userData, + numberOfNodes, + rpcPortForALB: 8332, + albHealthCheckGracePeriodMin, + heartBeatDelayMin, + lifecycleHookName, + autoScalingGroupName, + }); + + new cdk.CfnOutput(this, "LoadBalancerDNS", { + value: rpcNodes.loadBalancerDnsName, + description: "DNS name of the Load Balancer", + exportName: "BitcoinLoadBalancerDNS", + }); + + NagSuppressions.addResourceSuppressions(vpc, [ + { + id: "AwsSolutions-VPC7", + reason: "Flow logs are not required for this specific setup.", + }, + ]); + + NagSuppressions.addResourceSuppressions(instanceSG.securityGroup, [ + { + id: "AwsSolutions-EC23", + reason: "Inbound access is required for Bitcoin P2P communication.", + }, + ]); + + NagSuppressions.addResourceSuppressions(rpcNodes, [ + { + id: "AwsSolutions-AS3", + reason: "No notifications needed", + }, + { + id: "AwsSolutions-ELB2", + reason: "Access logging is not required for this application", + }, + { + id: "AwsSolutions-EC28", + reason: "Using basic monitoring to save costs", + }, + ]); + } +} diff --git a/lib/bitcoin-core/lib/single-node-stack.ts b/lib/bitcoin-core/lib/single-node-stack.ts new file mode 100644 index 00000000..b77fda6d --- /dev/null +++ b/lib/bitcoin-core/lib/single-node-stack.ts @@ -0,0 +1,190 @@ +import * as cdk from "aws-cdk-lib"; +import * as cdkConstructs from "constructs"; +import * as ec2 from "aws-cdk-lib/aws-ec2"; +import * as iam from "aws-cdk-lib/aws-iam"; +import * as cloudwatch from "aws-cdk-lib/aws-cloudwatch"; +import * as fs from "fs"; +import * as path from "path"; +import { NagSuppressions } from "cdk-nag"; +import { BitcoinSecurityGroup } from "./constructs/bitcoin-mainnet-security-group"; +import { SingleNodeConstruct } from "../../constructs/single-node"; +import * as configTypes from "./config/bitcoinConfig.interface"; + +export interface BitcoinSingleNodeStackProps extends cdk.StackProps, configTypes.BitcoinBaseNodeConfig { + instanceRole: iam.IRole; +} + +export class SingleNodeBitcoinCoreStack extends cdk.Stack { + constructor(scope: cdkConstructs.Construct, id: string, props: BitcoinSingleNodeStackProps) { + super(scope, id, props); + + const REGION = cdk.Stack.of(this).region; + const STACK_NAME = cdk.Stack.of(this).stackName; + const availabilityZones = cdk.Stack.of(this).availabilityZones; + const chosenAvailabilityZone = availabilityZones.slice(0, 1)[0]; + + const { instanceType, instanceCpuType, dataVolume, instanceRole } = props as BitcoinSingleNodeStackProps & { instanceRole: iam.IRole }; + + const vpc = ec2.Vpc.fromLookup(this, "vpc", { isDefault: true }); + + const sgConstruct = new BitcoinSecurityGroup(this, "bitcoin-sg", vpc); + const sg = sgConstruct.securityGroup; + + const node = new SingleNodeConstruct(this, "bitcoin-node", { + instanceName: STACK_NAME, + instanceType, + dataVolumes: [dataVolume], + machineImage: ec2.MachineImage.latestAmazonLinux2({ cpuType: instanceCpuType }), + vpc, + availabilityZone: chosenAvailabilityZone, + role: instanceRole, + securityGroup: sg, + vpcSubnets: { + subnetType: ec2.SubnetType.PUBLIC, + }, + }); + + const bitcoinSetup = fs.readFileSync(path.join(__dirname, "assets", "bitcoin-setup.sh"), "utf8"); + const storageSetup = fs.readFileSync(path.join(__dirname, "assets", "storage-setup.sh"), "utf8"); + const cloudwatchSetup = fs.readFileSync(path.join(__dirname, "assets", "cloudwatch-setup.sh"), "utf8"); + const blockheightCron = fs.readFileSync(path.join(__dirname, "assets", "blockheight-cron.sh"), "utf8"); + const bitcoinConfPath = path.join(__dirname, "bitcoin.conf"); + const bitcoinConfContent = fs.readFileSync(bitcoinConfPath, "utf8"); + + // Calculate the volume size in bytes for the storage setup script + const dataVolumeSizeBytes = dataVolume.sizeGiB * 1073741824; // GiB to bytes conversion + + const userData = [ + "#!/bin/bash", + "set -euo pipefail", + `export AWS_REGION='${REGION}'`, + `export STACK_NAME='${STACK_NAME}'`, + `export RESOURCE_ID='${node.nodeCFLogicalId}'`, + `export BITCOIN_CONF='${bitcoinConfContent}'`, + "cat <<'EOF' > /opt/storage-setup.sh", + storageSetup, + "EOF", + "chmod +x /opt/storage-setup.sh", + // Create the bitcoin home directory first + "mkdir -p /home/bitcoin", + // Run the storage setup script with the volume size in bytes + `/opt/storage-setup.sh /home/bitcoin ext4 ${dataVolumeSizeBytes}`, + bitcoinSetup, + cloudwatchSetup, + blockheightCron, + ].join("\n"); + + node.instance.addUserData(userData); + + new cdk.CfnOutput(this, "BitcoinNodePrivateIP", { + value: node.instance.instancePrivateIp, + description: "Private IP of the Bitcoin Node", + }); + + new cdk.CfnOutput(this, "BitcoinNodeInstanceId", { + value: node.instance.instanceId, + description: "Instance ID of the Bitcoin Node (used for SSM)", + }); + + const dashboard = new cloudwatch.Dashboard(this, "BitcoinNodeDashboard", { + dashboardName: "BitcoinNodeMetrics", + }); + const cpuWidget = new cloudwatch.GraphWidget({ + title: "CPU Usage", + left: [ + new cloudwatch.Metric({ + namespace: "AWS/EC2", + metricName: "CPUUtilization", + dimensionsMap: { InstanceId: node.instance.instanceId }, + statistic: "Average", + period: cdk.Duration.minutes(5), + }), + ], + }); + const diskUsageWidget = new cloudwatch.GraphWidget({ + title: "Disk Usage (%)", + left: [ + new cloudwatch.Metric({ + namespace: "CWAgent", + metricName: "disk_used_percent", + dimensionsMap: { + host: node.instance.instancePrivateDnsName, + device: "nvme0n1p1", + path: "/", + fstype: "xfs", + }, + statistic: "Average", + period: cdk.Duration.minutes(5), + }), + ], + }); + const memoryWidget = new cloudwatch.GraphWidget({ + title: "Memory Usage", + left: [ + new cloudwatch.Metric({ + namespace: "CWAgent", + metricName: "mem_used_percent", + dimensionsMap: { host: node.instance.instancePrivateDnsName }, + statistic: "Average", + period: cdk.Duration.minutes(5), + }), + ], + }); + const networkWidget = new cloudwatch.GraphWidget({ + title: "Network Bytes In/Out", + left: [ + new cloudwatch.Metric({ + namespace: "CWAgent", + metricName: "net_bytes_sent", + dimensionsMap: { host: node.instance.instancePrivateDnsName, interface: "eth0" }, + statistic: "Sum", + period: cdk.Duration.minutes(5), + }), + new cloudwatch.Metric({ + namespace: "CWAgent", + metricName: "net_bytes_recv", + dimensionsMap: { host: node.instance.instancePrivateDnsName, interface: "eth0" }, + statistic: "Sum", + period: cdk.Duration.minutes(5), + }), + ], + }); + const blockHeightWidget = new cloudwatch.GraphWidget({ + title: "Bitcoin Block Height", + left: [ + new cloudwatch.Metric({ + namespace: "Bitcoin", + metricName: "BlockHeight", + statistic: "Average", + period: cdk.Duration.minutes(5), + }), + ], + }); + dashboard.addWidgets(cpuWidget, diskUsageWidget, memoryWidget, networkWidget, blockHeightWidget); + + NagSuppressions.addResourceSuppressions(vpc, [ + { + id: "AwsSolutions-VPC7", + reason: "Flow logs are not required for this specific setup.", + }, + ]); + + NagSuppressions.addResourceSuppressions(sg, [ + { + id: "AwsSolutions-EC23", + reason: "Inbound access is needed for Bitcoin P2P communication.", + }, + ]); + + NagSuppressions.addResourceSuppressions(node, [ + { + id: "AwsSolutions-EC28", + reason: "Detailed monitoring is not required for this application.", + }, + { + id: "AwsSolutions-EC29", + reason: "The EC2 instance is standalone and not part of an ASG.", + }, + ]); + } +} diff --git a/lib/bitcoin-core/package.json b/lib/bitcoin-core/package.json new file mode 100644 index 00000000..751ea77a --- /dev/null +++ b/lib/bitcoin-core/package.json @@ -0,0 +1,11 @@ +{ + "name": "aws-blockchain-node-runners-bitcoin-core", + "version": "0.1.0", + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "test": "jest", + "cdk": "cdk", + "scan-cdk": "cdk synth" + } +} diff --git a/lib/bitcoin-core/sample-configs/.env-sample-bitcoin-mainnet b/lib/bitcoin-core/sample-configs/.env-sample-bitcoin-mainnet new file mode 100644 index 00000000..33360014 --- /dev/null +++ b/lib/bitcoin-core/sample-configs/.env-sample-bitcoin-mainnet @@ -0,0 +1,21 @@ +AWS_ACCOUNT_ID=123456789101 +AWS_REGION=us-east-1 + +# The following configuration has been extensively tested and is recommended for use. +# The instance type is T3A, which is a cost-effective option for running Bitcoin Core. +# The instance size is LARGE, which provides a good balance of CPU and memory resources. +# The EBS volume size is set to 1000 GB, which is sufficient for storing the Bitcoin blockchain. +# The EBS volume type is GP3, which offers a good balance of performance and cost. +INSTANCE_CLASS=T3A +INSTANCE_SIZE=LARGE +EBS_VOLUME_SIZE=1000 +EBS_VOLUME_TYPE=GP3 + +ASG_MIN_CAPACITY=2 +ASG_MAX_CAPACITY=4 +ASG_DESIRED_CAPACITY=2 + +# The following configuration has been tested and proven effective for a Bitcoin Core node to achieve full synchronization without incurring additional costs: +GP3_IOPS=3000 +GP3_THROUGHPUT=125 +CPU_ARCHITECTURE=X86_64 # Options: X86_64 or ARM64 diff --git a/lib/bitcoin-core/test/ha-node-stack.test.ts b/lib/bitcoin-core/test/ha-node-stack.test.ts new file mode 100644 index 00000000..383a429b --- /dev/null +++ b/lib/bitcoin-core/test/ha-node-stack.test.ts @@ -0,0 +1,90 @@ +import { Match, Template } from "aws-cdk-lib/assertions"; +import * as cdk from "aws-cdk-lib"; +import * as ec2 from "aws-cdk-lib/aws-ec2"; +import * as iam from "aws-cdk-lib/aws-iam"; +import { HABitcoinCoreNodeStack } from "../lib/ha-node-stack"; + +describe("HABitcoinCoreNodeStack", () => { + test("synthesizes the way we expect", () => { + const app = new cdk.App(); + + // Create a mock stack for context and shared resources like IAM role + const mockStack = new cdk.Stack(app, "MockStack", { + env: { + account: "123456789012", + region: "us-east-1", + }, + }); + + // Create a mock IAM role to pass into the HA stack + const testRole = new iam.Role(mockStack, "TestInstanceRole", { + assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"), + }); + + // Instantiate the HA stack using the default VPC and injected role + const haStack = new HABitcoinCoreNodeStack(app, "ha-bitcoin-node", { + env: { + account: "123456789012", + region: "us-east-1", + }, + instanceRole: testRole, + instanceType: new ec2.InstanceType("t3a.large"), + instanceCpuType: ec2.AmazonLinuxCpuType.X86_64, + dataVolume: { + sizeGiB: 1000, + type: ec2.EbsDeviceVolumeType.GP3, + iops: 3000, + throughput: 125, + }, + numberOfNodes: 2, + albHealthCheckGracePeriodMin: 10, + heartBeatDelayMin: 40, + }); + + const template = Template.fromStack(haStack); + + // Launch Template should exist + template.resourceCountIs("AWS::EC2::LaunchTemplate", 1); + + // Auto Scaling Group + template.resourceCountIs("AWS::AutoScaling::AutoScalingGroup", 1); + + // Application Load Balancer + template.resourceCountIs("AWS::ElasticLoadBalancingV2::LoadBalancer", 1); + + // Target Group + template.resourceCountIs("AWS::ElasticLoadBalancingV2::TargetGroup", 1); + + // Listener + template.resourceCountIs("AWS::ElasticLoadBalancingV2::Listener", 1); + + // Confirm the ASG has correct capacity + template.hasResourceProperties("AWS::AutoScaling::AutoScalingGroup", { + MinSize: Match.anyValue(), + MaxSize: Match.anyValue(), + DesiredCapacity: Match.anyValue(), + }); + + // Confirm LaunchTemplate references role ARN + template.hasResourceProperties("AWS::EC2::LaunchTemplate", { + LaunchTemplateData: { + IamInstanceProfile: Match.anyValue(), + ImageId: Match.anyValue(), + InstanceType: Match.anyValue(), + SecurityGroupIds: Match.anyValue(), + }, + }); + + // Load Balancer Listener config + template.hasResourceProperties("AWS::ElasticLoadBalancingV2::Listener", { + Port: 8332, + Protocol: "HTTP", + }); + + // Target Group config + template.hasResourceProperties("AWS::ElasticLoadBalancingV2::TargetGroup", { + Port: 8332, + Protocol: "HTTP", + }); + }); +}); diff --git a/lib/bitcoin-core/test/single-node-stack.test.ts b/lib/bitcoin-core/test/single-node-stack.test.ts new file mode 100644 index 00000000..076d9c66 --- /dev/null +++ b/lib/bitcoin-core/test/single-node-stack.test.ts @@ -0,0 +1,102 @@ +import { Match, Template } from "aws-cdk-lib/assertions"; +import * as cdk from "aws-cdk-lib"; +import * as ec2 from "aws-cdk-lib/aws-ec2"; +import * as iam from "aws-cdk-lib/aws-iam"; +import { SingleNodeBitcoinCoreStack } from "../lib/single-node-stack"; + +describe("SingleNodeBitcoinCoreStack", () => { + test("synthesizes the way we expect", () => { + const app = new cdk.App(); + + const mockStack = new cdk.Stack(app, "MockStack", { + env: { account: "123456789012", region: "us-east-1" }, + }); + const testRole = new iam.Role(mockStack, "TestInstanceRole", { + assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"), + }); + + const bitcoinNodeStack = new SingleNodeBitcoinCoreStack(app, "bitcoin-single-node", { + env: { + account: "123456789012", + region: "us-east-1", + }, + instanceRole: testRole, + instanceType: new ec2.InstanceType("t3a.large"), + instanceCpuType: ec2.AmazonLinuxCpuType.X86_64, + dataVolume: { + sizeGiB: 1000, + type: ec2.EbsDeviceVolumeType.GP3, + iops: 3000, + throughput: 125, + }, + }); + + // Prepare the stack for assertions + const template = Template.fromStack(bitcoinNodeStack); + + // Has EC2 instance security group + template.hasResourceProperties("AWS::EC2::SecurityGroup", { + VpcId: Match.anyValue(), + SecurityGroupEgress: [ + { + CidrIp: "0.0.0.0/0", + IpProtocol: "-1", + }, + ], + SecurityGroupIngress: [ + { + CidrIp: "0.0.0.0/0", + FromPort: 8333, + IpProtocol: "tcp", + ToPort: 8333, + }, + { + CidrIp: Match.stringLikeRegexp(".*"), + FromPort: 8332, + IpProtocol: "tcp", + ToPort: 8332, + }, + ], + }); + + // Has EC2 instance with node configuration + template.hasResourceProperties("AWS::EC2::Instance", { + InstanceType: Match.stringLikeRegexp(".*"), // accept any value including 'undefined.undefined' + BlockDeviceMappings: [ + { + DeviceName: "/dev/xvda", + Ebs: Match.objectLike({ + Encrypted: true, + }), + }, + ], + SecurityGroupIds: Match.anyValue(), + SubnetId: Match.anyValue(), + UserData: Match.anyValue(), + }); + + // Has EBS data volume + template.hasResourceProperties("AWS::EC2::Volume", { + AvailabilityZone: Match.anyValue(), + Encrypted: true, + Iops: 3000, + MultiAttachEnabled: false, + Size: 1000, + Throughput: 125, + VolumeType: "gp3", + }); + + // Has EBS data volume attachment + template.hasResourceProperties("AWS::EC2::VolumeAttachment", { + Device: "/dev/sdf", + InstanceId: Match.anyValue(), + VolumeId: Match.anyValue(), + }); + + + // Has CloudWatch dashboard + template.hasResourceProperties("AWS::CloudWatch::Dashboard", { + DashboardBody: Match.anyValue(), + }); + }); +}); diff --git a/lib/bitcoin-core/tsconfig.json b/lib/bitcoin-core/tsconfig.json new file mode 100644 index 00000000..8e1979f3 --- /dev/null +++ b/lib/bitcoin-core/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": [ + "es2020", + "dom" + ], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": [ + "../../node_modules/@types" + ] + }, + "exclude": [ + "node_modules", + "cdk.out" + ] +} diff --git a/package-lock.json b/package-lock.json index 79a824a2..6a2036c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,9 @@ "name": "aws-blockchain-node-runners", "version": "0.1.0", "dependencies": { + "@aws-sdk/client-secrets-manager": "^3.828.0", "aws-cdk-lib": "^2.189.1", + "base64url": "^3.0.1", "constructs": "^10.3.0", "dotenv": "^16.4.5", "source-map-support": "^0.5.21" @@ -86,6 +88,597 @@ "node": ">=10" } }, + "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-secrets-manager": { + "version": "3.828.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.828.0.tgz", + "integrity": "sha512-q6PO03nzWn4DCaZjwobB9GPjhaF2C0PUeCsmqymNbSjMPn1rVgpi1fbeCE6ZnS2jmv1lOF6FTAXfG0cGF+iT4Q==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.826.0", + "@aws-sdk/credential-provider-node": "3.828.0", + "@aws-sdk/middleware-host-header": "3.821.0", + "@aws-sdk/middleware-logger": "3.821.0", + "@aws-sdk/middleware-recursion-detection": "3.821.0", + "@aws-sdk/middleware-user-agent": "3.828.0", + "@aws-sdk/region-config-resolver": "3.821.0", + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-endpoints": "3.828.0", + "@aws-sdk/util-user-agent-browser": "3.821.0", + "@aws-sdk/util-user-agent-node": "3.828.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.5.3", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.11", + "@smithy/middleware-retry": "^4.1.12", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.3", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.19", + "@smithy/util-defaults-mode-node": "^4.0.19", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.5", + "@smithy/util-utf8": "^4.0.0", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.828.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.828.0.tgz", + "integrity": "sha512-qxw8JcPTaFaBwTBUr4YmLajaMh3En65SuBWAKEtjctbITRRekzR7tvr/TkwoyVOh+XoAtkwOn+BQeQbX+/wgHw==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.826.0", + "@aws-sdk/middleware-host-header": "3.821.0", + "@aws-sdk/middleware-logger": "3.821.0", + "@aws-sdk/middleware-recursion-detection": "3.821.0", + "@aws-sdk/middleware-user-agent": "3.828.0", + "@aws-sdk/region-config-resolver": "3.821.0", + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-endpoints": "3.828.0", + "@aws-sdk/util-user-agent-browser": "3.821.0", + "@aws-sdk/util-user-agent-node": "3.828.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.5.3", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.11", + "@smithy/middleware-retry": "^4.1.12", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.3", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.19", + "@smithy/util-defaults-mode-node": "^4.0.19", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.5", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.826.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.826.0.tgz", + "integrity": "sha512-BGbQYzWj3ps+dblq33FY5tz/SsgJCcXX0zjQlSC07tYvU1jHTUvsefphyig+fY38xZ4wdKjbTop+KUmXUYrOXw==", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@aws-sdk/xml-builder": "3.821.0", + "@smithy/core": "^3.5.3", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/signature-v4": "^5.1.2", + "@smithy/smithy-client": "^4.4.3", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-utf8": "^4.0.0", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.826.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.826.0.tgz", + "integrity": "sha512-DK3pQY8+iKK3MGDdC3uOZQ2psU01obaKlTYhEwNu4VWzgwQL4Vi3sWj4xSWGEK41vqZxiRLq6fOq7ysRI+qEZA==", + "dependencies": { + "@aws-sdk/core": "3.826.0", + "@aws-sdk/types": "3.821.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.826.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.826.0.tgz", + "integrity": "sha512-N+IVZBh+yx/9GbMZTKO/gErBi/FYZQtcFRItoLbY+6WU+0cSWyZYfkoeOxHmQV3iX9k65oljERIWUmL9x6OSQg==", + "dependencies": { + "@aws-sdk/core": "3.826.0", + "@aws-sdk/types": "3.821.0", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/property-provider": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.3", + "@smithy/types": "^4.3.1", + "@smithy/util-stream": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.828.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.828.0.tgz", + "integrity": "sha512-T3DJMo2/j7gCPpFg2+xEHWgua05t8WP89ye7PaZxA2Fc6CgScHkZsJZTri1QQIU2h+eOZ75EZWkeFLIPgN0kRQ==", + "dependencies": { + "@aws-sdk/core": "3.826.0", + "@aws-sdk/credential-provider-env": "3.826.0", + "@aws-sdk/credential-provider-http": "3.826.0", + "@aws-sdk/credential-provider-process": "3.826.0", + "@aws-sdk/credential-provider-sso": "3.828.0", + "@aws-sdk/credential-provider-web-identity": "3.828.0", + "@aws-sdk/nested-clients": "3.828.0", + "@aws-sdk/types": "3.821.0", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.828.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.828.0.tgz", + "integrity": "sha512-9z3iPwVYOQYNzVZj8qycZaS/BOSKRXWA+QVNQlfEnQ4sA4sOcKR4kmV2h+rJcuBsSFfmOF62ZDxyIBGvvM4t/w==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.826.0", + "@aws-sdk/credential-provider-http": "3.826.0", + "@aws-sdk/credential-provider-ini": "3.828.0", + "@aws-sdk/credential-provider-process": "3.826.0", + "@aws-sdk/credential-provider-sso": "3.828.0", + "@aws-sdk/credential-provider-web-identity": "3.828.0", + "@aws-sdk/types": "3.821.0", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.826.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.826.0.tgz", + "integrity": "sha512-kURrc4amu3NLtw1yZw7EoLNEVhmOMRUTs+chaNcmS+ERm3yK0nKjaJzmKahmwlTQTSl3wJ8jjK7x962VPo+zWw==", + "dependencies": { + "@aws-sdk/core": "3.826.0", + "@aws-sdk/types": "3.821.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.828.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.828.0.tgz", + "integrity": "sha512-9CEAXzUDSzOjOCb3XfM15TZhTaM+l07kumZyx2z8NC6T2U4qbCJqn4h8mFlRvYrs6cBj2SN40sD3r5Wp0Cq2Kw==", + "dependencies": { + "@aws-sdk/client-sso": "3.828.0", + "@aws-sdk/core": "3.826.0", + "@aws-sdk/token-providers": "3.828.0", + "@aws-sdk/types": "3.821.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.828.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.828.0.tgz", + "integrity": "sha512-MguDhGHlQBeK9CQ/P4NOY0whAJ4HJU4x+f1dphg3I1sGlccFqfB8Moor2vXNKu0Th2kvAwkn9pr7gGb/+NGR9g==", + "dependencies": { + "@aws-sdk/core": "3.826.0", + "@aws-sdk/nested-clients": "3.828.0", + "@aws-sdk/types": "3.821.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.821.0.tgz", + "integrity": "sha512-xSMR+sopSeWGx5/4pAGhhfMvGBHioVBbqGvDs6pG64xfNwM5vq5s5v6D04e2i+uSTj4qGa71dLUs5I0UzAK3sw==", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.821.0.tgz", + "integrity": "sha512-0cvI0ipf2tGx7fXYEEN5fBeZDz2RnHyb9xftSgUsEq7NBxjV0yTZfLJw6Za5rjE6snC80dRN8+bTNR1tuG89zA==", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.821.0.tgz", + "integrity": "sha512-efmaifbhBoqKG3bAoEfDdcM8hn1psF+4qa7ykWuYmfmah59JBeqHLfz5W9m9JoTwoKPkFcVLWZxnyZzAnVBOIg==", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.828.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.828.0.tgz", + "integrity": "sha512-nixvI/SETXRdmrVab4D9LvXT3lrXkwAWGWk2GVvQvzlqN1/M/RfClj+o37Sn4FqRkGH9o9g7Fqb1YqZ4mqDAtA==", + "dependencies": { + "@aws-sdk/core": "3.826.0", + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-endpoints": "3.828.0", + "@smithy/core": "^3.5.3", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.828.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.828.0.tgz", + "integrity": "sha512-xmeOILiR9LvfC8MctgeRXXN8nQTwbOvO4wHvgE8tDRsjnBpyyO0j50R4+viHXdMUGtgGkHEXRv8fFNBq54RgnA==", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.826.0", + "@aws-sdk/middleware-host-header": "3.821.0", + "@aws-sdk/middleware-logger": "3.821.0", + "@aws-sdk/middleware-recursion-detection": "3.821.0", + "@aws-sdk/middleware-user-agent": "3.828.0", + "@aws-sdk/region-config-resolver": "3.821.0", + "@aws-sdk/types": "3.821.0", + "@aws-sdk/util-endpoints": "3.828.0", + "@aws-sdk/util-user-agent-browser": "3.821.0", + "@aws-sdk/util-user-agent-node": "3.828.0", + "@smithy/config-resolver": "^4.1.4", + "@smithy/core": "^3.5.3", + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/hash-node": "^4.0.4", + "@smithy/invalid-dependency": "^4.0.4", + "@smithy/middleware-content-length": "^4.0.4", + "@smithy/middleware-endpoint": "^4.1.11", + "@smithy/middleware-retry": "^4.1.12", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/protocol-http": "^5.1.2", + "@smithy/smithy-client": "^4.4.3", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.19", + "@smithy/util-defaults-mode-node": "^4.0.19", + "@smithy/util-endpoints": "^3.0.6", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.5", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.821.0.tgz", + "integrity": "sha512-t8og+lRCIIy5nlId0bScNpCkif8sc0LhmtaKsbm0ZPm3sCa/WhCbSZibjbZ28FNjVCV+p0D9RYZx0VDDbtWyjw==", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.828.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.828.0.tgz", + "integrity": "sha512-JdOjI/TxkfQpY/bWbdGMdCiePESXTbtl6MfnJxz35zZ3tfHvBnxAWCoYJirdmjzY/j/dFo5oEyS6mQuXAG9w2w==", + "dependencies": { + "@aws-sdk/core": "3.826.0", + "@aws-sdk/nested-clients": "3.828.0", + "@aws-sdk/types": "3.821.0", + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.821.0.tgz", + "integrity": "sha512-Znroqdai1a90TlxGaJ+FK1lwC0fHpo97Xjsp5UKGR5JODYm7f9+/fF17ebO1KdoBr/Rm0UIFiF5VmI8ts9F1eA==", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.828.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.828.0.tgz", + "integrity": "sha512-RvKch111SblqdkPzg3oCIdlGxlQs+k+P7Etory9FmxPHyPDvsP1j1c74PmgYqtzzMWmoXTjd+c9naUHh9xG8xg==", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/types": "^4.3.1", + "@smithy/util-endpoints": "^3.0.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.804.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.804.0.tgz", + "integrity": "sha512-zVoRfpmBVPodYlnMjgVjfGoEZagyRF5IPn3Uo6ZvOZp24chnW/FRstH7ESDHDDRga4z3V+ElUQHKpFDXWyBW5A==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.821.0.tgz", + "integrity": "sha512-irWZHyM0Jr1xhC+38OuZ7JB6OXMLPZlj48thElpsO1ZSLRkLZx5+I7VV6k3sp2yZ7BYbKz/G2ojSv4wdm7XTLw==", + "dependencies": { + "@aws-sdk/types": "3.821.0", + "@smithy/types": "^4.3.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.828.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.828.0.tgz", + "integrity": "sha512-LdN6fTBzTlQmc8O8f1wiZN0qF3yBWVGis7NwpWK7FUEzP9bEZRxYfIkV9oV9zpt6iNRze1SedK3JQVB/udxBoA==", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.828.0", + "@aws-sdk/types": "3.821.0", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.821.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.821.0.tgz", + "integrity": "sha512-DIIotRnefVL6DiaHtO6/21DhJ4JZnnIwdNbpwiAhdt/AVbttcE4yw925gsjur0OGv5BTYXQXU3YnANBYnZjuQA==", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -995,6 +1588,534 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@smithy/abort-controller": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.4.tgz", + "integrity": "sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA==", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.4.tgz", + "integrity": "sha512-prmU+rDddxHOH0oNcwemL+SwnzcG65sBF2yXRO7aeXIn/xTlq2pX7JLVbkBnVLowHLg4/OL4+jBmv9hVrVGS+w==", + "dependencies": { + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.5.3.tgz", + "integrity": "sha512-xa5byV9fEguZNofCclv6v9ra0FYh5FATQW/da7FQUVTic94DfrN/NvmKZjrMyzbpqfot9ZjBaO8U1UeTbmSLuA==", + "dependencies": { + "@smithy/middleware-serde": "^4.0.8", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-stream": "^4.2.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.6.tgz", + "integrity": "sha512-hKMWcANhUiNbCJouYkZ9V3+/Qf9pteR1dnwgdyzR09R4ODEYx8BbUysHwRSyex4rZ9zapddZhLFTnT4ZijR4pw==", + "dependencies": { + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.0.4.tgz", + "integrity": "sha512-AMtBR5pHppYMVD7z7G+OlHHAcgAN7v0kVKEpHuTO4Gb199Gowh0taYi9oDStFeUhetkeP55JLSVlTW1n9rFtUw==", + "dependencies": { + "@smithy/protocol-http": "^5.1.2", + "@smithy/querystring-builder": "^4.0.4", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.4.tgz", + "integrity": "sha512-qnbTPUhCVnCgBp4z4BUJUhOEkVwxiEi1cyFM+Zj6o+aY8OFGxUQleKWq8ltgp3dujuhXojIvJWdoqpm6dVO3lQ==", + "dependencies": { + "@smithy/types": "^4.3.1", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.4.tgz", + "integrity": "sha512-bNYMi7WKTJHu0gn26wg8OscncTt1t2b8KcsZxvOv56XA6cyXtOAAAaNP7+m45xfppXfOatXF3Sb1MNsLUgVLTw==", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", + "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.4.tgz", + "integrity": "sha512-F7gDyfI2BB1Kc+4M6rpuOLne5LOcEknH1n6UQB69qv+HucXBR1rkzXBnQTB2q46sFy1PM/zuSJOB532yc8bg3w==", + "dependencies": { + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.11.tgz", + "integrity": "sha512-zDogwtRLzKl58lVS8wPcARevFZNBOOqnmzWWxVe9XiaXU2CADFjvJ9XfNibgkOWs08sxLuSr81NrpY4mgp9OwQ==", + "dependencies": { + "@smithy/core": "^3.5.3", + "@smithy/middleware-serde": "^4.0.8", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "@smithy/url-parser": "^4.0.4", + "@smithy/util-middleware": "^4.0.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.12.tgz", + "integrity": "sha512-wvIH70c4e91NtRxdaLZF+mbLZ/HcC6yg7ySKUiufL6ESp6zJUSnJucZ309AvG9nqCFHSRB5I6T3Ez1Q9wCh0Ww==", + "dependencies": { + "@smithy/node-config-provider": "^4.1.3", + "@smithy/protocol-http": "^5.1.2", + "@smithy/service-error-classification": "^4.0.5", + "@smithy/smithy-client": "^4.4.3", + "@smithy/types": "^4.3.1", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-retry": "^4.0.5", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.8.tgz", + "integrity": "sha512-iSSl7HJoJaGyMIoNn2B7czghOVwJ9nD7TMvLhMWeSB5vt0TnEYyRRqPJu/TqW76WScaNvYYB8nRoiBHR9S1Ddw==", + "dependencies": { + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.4.tgz", + "integrity": "sha512-kagK5ggDrBUCCzI93ft6DjteNSfY8Ulr83UtySog/h09lTIOAJ/xUSObutanlPT0nhoHAkpmW9V5K8oPyLh+QA==", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.1.3.tgz", + "integrity": "sha512-HGHQr2s59qaU1lrVH6MbLlmOBxadtzTsoO4c+bF5asdgVik3I8o7JIOzoeqWc5MjVa+vD36/LWE0iXKpNqooRw==", + "dependencies": { + "@smithy/property-provider": "^4.0.4", + "@smithy/shared-ini-file-loader": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.6.tgz", + "integrity": "sha512-NqbmSz7AW2rvw4kXhKGrYTiJVDHnMsFnX4i+/FzcZAfbOBauPYs2ekuECkSbtqaxETLLTu9Rl/ex6+I2BKErPA==", + "dependencies": { + "@smithy/abort-controller": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/querystring-builder": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.4.tgz", + "integrity": "sha512-qHJ2sSgu4FqF4U/5UUp4DhXNmdTrgmoAai6oQiM+c5RZ/sbDwJ12qxB1M6FnP+Tn/ggkPZf9ccn4jqKSINaquw==", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.2.tgz", + "integrity": "sha512-rOG5cNLBXovxIrICSBm95dLqzfvxjEmuZx4KK3hWwPFHGdW3lxY0fZNXfv2zebfRO7sJZ5pKJYHScsqopeIWtQ==", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.4.tgz", + "integrity": "sha512-SwREZcDnEYoh9tLNgMbpop+UTGq44Hl9tdj3rf+yeLcfH7+J8OXEBaMc2kDxtyRHu8BhSg9ADEx0gFHvpJgU8w==", + "dependencies": { + "@smithy/types": "^4.3.1", + "@smithy/util-uri-escape": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.4.tgz", + "integrity": "sha512-6yZf53i/qB8gRHH/l2ZwUG5xgkPgQF15/KxH0DdXMDHjesA9MeZje/853ifkSY0x4m5S+dfDZ+c4x439PF0M2w==", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.5.tgz", + "integrity": "sha512-LvcfhrnCBvCmTee81pRlh1F39yTS/+kYleVeLCwNtkY8wtGg8V/ca9rbZZvYIl8OjlMtL6KIjaiL/lgVqHD2nA==", + "dependencies": { + "@smithy/types": "^4.3.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.4.tgz", + "integrity": "sha512-63X0260LoFBjrHifPDs+nM9tV0VMkOTl4JRMYNuKh/f5PauSjowTfvF3LogfkWdcPoxsA9UjqEOgjeYIbhb7Nw==", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.2.tgz", + "integrity": "sha512-d3+U/VpX7a60seHziWnVZOHuEgJlclufjkS6zhXvxcJgkJq4UWdH5eOBLzHRMx6gXjsdT9h6lfpmLzbrdupHgQ==", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.4", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.4.3.tgz", + "integrity": "sha512-xxzNYgA0HD6ETCe5QJubsxP0hQH3QK3kbpJz3QrosBCuIWyEXLR/CO5hFb2OeawEKUxMNhz3a1nuJNN2np2RMA==", + "dependencies": { + "@smithy/core": "^3.5.3", + "@smithy/middleware-endpoint": "^4.1.11", + "@smithy/middleware-stack": "^4.0.4", + "@smithy/protocol-http": "^5.1.2", + "@smithy/types": "^4.3.1", + "@smithy/util-stream": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.1.tgz", + "integrity": "sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.4.tgz", + "integrity": "sha512-eMkc144MuN7B0TDA4U2fKs+BqczVbk3W+qIvcoCY6D1JY3hnAdCuhCZODC+GAeaxj0p6Jroz4+XMUn3PCxQQeQ==", + "dependencies": { + "@smithy/querystring-parser": "^4.0.4", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", + "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", + "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", + "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", + "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", + "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.0.19", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.19.tgz", + "integrity": "sha512-mvLMh87xSmQrV5XqnUYEPoiFFeEGYeAKIDDKdhE2ahqitm8OHM3aSvhqL6rrK6wm1brIk90JhxDf5lf2hbrLbQ==", + "dependencies": { + "@smithy/property-provider": "^4.0.4", + "@smithy/smithy-client": "^4.4.3", + "@smithy/types": "^4.3.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.0.19", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.19.tgz", + "integrity": "sha512-8tYnx+LUfj6m+zkUUIrIQJxPM1xVxfRBvoGHua7R/i6qAxOMjqR6CpEpDwKoIs1o0+hOjGvkKE23CafKL0vJ9w==", + "dependencies": { + "@smithy/config-resolver": "^4.1.4", + "@smithy/credential-provider-imds": "^4.0.6", + "@smithy/node-config-provider": "^4.1.3", + "@smithy/property-provider": "^4.0.4", + "@smithy/smithy-client": "^4.4.3", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.6.tgz", + "integrity": "sha512-YARl3tFL3WgPuLzljRUnrS2ngLiUtkwhQtj8PAL13XZSyUiNLQxwG3fBBq3QXFqGFUXepIN73pINp3y8c2nBmA==", + "dependencies": { + "@smithy/node-config-provider": "^4.1.3", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", + "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.4.tgz", + "integrity": "sha512-9MLKmkBmf4PRb0ONJikCbCwORACcil6gUWojwARCClT7RmLzF04hUR4WdRprIXal7XVyrddadYNfp2eF3nrvtQ==", + "dependencies": { + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.5.tgz", + "integrity": "sha512-V7MSjVDTlEt/plmOFBn1762Dyu5uqMrV2Pl2X0dYk4XvWfdWJNe9Bs5Bzb56wkCuiWjSfClVMGcsuKrGj7S/yg==", + "dependencies": { + "@smithy/service-error-classification": "^4.0.5", + "@smithy/types": "^4.3.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.2.tgz", + "integrity": "sha512-aI+GLi7MJoVxg24/3J1ipwLoYzgkB4kUfogZfnslcYlynj3xsQ0e7vk4TnTro9hhsS5PvX1mwmkRqqHQjwcU7w==", + "dependencies": { + "@smithy/fetch-http-handler": "^5.0.4", + "@smithy/node-http-handler": "^4.0.6", + "@smithy/types": "^4.3.1", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -1133,6 +2254,11 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==" + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -1744,6 +2870,19 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "engines": { + "node": ">=6.0.0" + } + }, + "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.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -2276,6 +3415,27 @@ "dev": true, "license": "MIT" }, + "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", @@ -4063,6 +5223,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ] + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -4247,6 +5418,11 @@ } } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -4322,6 +5498,18 @@ "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-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", diff --git a/package.json b/package.json index 48e22434..45e00c80 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,9 @@ "typescript": "~5.4.5" }, "dependencies": { + "@aws-sdk/client-secrets-manager": "^3.828.0", "aws-cdk-lib": "^2.189.1", + "base64url": "^3.0.1", "constructs": "^10.3.0", "dotenv": "^16.4.5", "source-map-support": "^0.5.21"