From 48bc6beca27fcb97384bb723b68790b9758ae6a2 Mon Sep 17 00:00:00 2001 From: NichArchA82 <64152648+NichArchA82@users.noreply.github.com> Date: Sun, 29 Sep 2024 15:38:26 +0000 Subject: [PATCH 1/2] chore: refactor hetzner code --- .../src/util/hetzner/get-hetzner-images.js | 29 ++ .../src/util/hetzner/get-servers.js | 19 + .../src/util/hetzner/hetzner-servers.js | 401 ++++++++++++++++++ 3 files changed, 449 insertions(+) create mode 100644 command-handler/src/util/hetzner/get-hetzner-images.js create mode 100644 command-handler/src/util/hetzner/get-servers.js create mode 100644 command-handler/src/util/hetzner/hetzner-servers.js diff --git a/command-handler/src/util/hetzner/get-hetzner-images.js b/command-handler/src/util/hetzner/get-hetzner-images.js new file mode 100644 index 0000000..681552e --- /dev/null +++ b/command-handler/src/util/hetzner/get-hetzner-images.js @@ -0,0 +1,29 @@ +import axios from "axios" +import logger from "../logger.js" +import axiosError from "../axios-error-handler.js"; + +const log = logger(); + +export default async function getHetznerImages() { + const response = await axios.get(`https://api.hetzner.cloud/v1/images?per_page=5&page=1&sort=created:desc&type=snapshot`, { + headers: { + 'Authorization': `Bearer ${process.env.HETZNER_API_TOKEN}` + } + }) + .catch(error => { + log.error('Failed to get images from hetzner', axiosError(error)); + }); + + const images = response.data.images; + + try { + if (!Array.isArray(images)) { + throw new Error('images is not an array'); + } + } catch (error) { + log.error({message: error.message, stack: error.stack}); + return null; + } + + return images; +} \ No newline at end of file diff --git a/command-handler/src/util/hetzner/get-servers.js b/command-handler/src/util/hetzner/get-servers.js new file mode 100644 index 0000000..f24d784 --- /dev/null +++ b/command-handler/src/util/hetzner/get-servers.js @@ -0,0 +1,19 @@ +import 'dotenv/config'; +import axios from 'axios'; +import axiosError from '../axios-error-handler.js'; +import logger from '../logger.js'; + +const log = logger(); + +export default async function getServer(serverId = "") { + const data = await axios.get(`https://api.hetzner.cloud/v1/servers/${serverId}`, { + headers: { + 'Authorization': `Bearer ${process.env.HETZNER_API_TOKEN}` + } + }) + .catch(error => { + log.error('Failed to get servers from hetzner', axiosError(error)); + }); + + return data; +} \ No newline at end of file diff --git a/command-handler/src/util/hetzner/hetzner-servers.js b/command-handler/src/util/hetzner/hetzner-servers.js new file mode 100644 index 0000000..190ebab --- /dev/null +++ b/command-handler/src/util/hetzner/hetzner-servers.js @@ -0,0 +1,401 @@ +import logger from '../logger.js'; +import axios from 'axios'; +import 'dotenv/config'; +import formatUser from '../format-user.js'; +import getDevices from '../get-devices-info.js'; +import getServer from './get-servers.js'; +import configUserData from '../get-user-data.js'; +import axiosError from '../axios-error-handler.js'; +import getHetznerImages from './get-hetzner-images.js'; +import { uniqueNamesGenerator, colors, animals } from 'unique-names-generator'; + +const log = logger(); + +const delay = (ms) => { + return new Promise(resolve => setTimeout(resolve, ms)); + } + +const region = "hel1"; +const serverType = 'cpx41'; + +export default { + //create the hetzner server + createServer: async ({ app, body, imageID, imageName }) => { + //auto generate the name + const serverName = uniqueNamesGenerator({ + dictionaries: [ colors, animals ], + separator: '-' + }); + + // Call the users.info method using the WebClient + let info; + try { + info = await app.client.users.info({ + user: body.user.id + }); + } catch (error) { + log.error('There was an error calling the user.info method in slack', error); + + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + text: `Failed to get user info from slack` + }); + + return; + } + + const userEmail = formatUser(info.user.profile.email); + + //post a status message + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + text: `Creating the server with image: ${imageName} This will take about 5 minutes.` + }); + + //hetzner api to create the server + let serverRes; + try { + serverRes = await axios.post('https://api.hetzner.cloud/v1/servers', + { + "automount": false, + "image": imageID, + "labels": { + "owner": userEmail + }, + "location": region, + "name": serverName, + "public_net": { + "enable_ipv4": true, + "enable_ipv6": false + }, + "server_type": serverType, + "start_after_create": true, + "user_data": configUserData(serverName) + }, { + headers: { + 'Authorization': `Bearer ${process.env.HETZNER_API_TOKEN}`, + 'Content-Type': 'application/json' + } + }); + } catch (error) { + log.error('There was an error creating the server', axiosError(error)); + + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + text: `Failed to create a server in hetzner.` + }); + + return; + } + + let maxRetries = 20; + let attempts; + + for (attempts = 1; attempts <= maxRetries; attempts++) { + //wait 30 seconds + await delay(1000 * 30); + + const server = await getServer(serverRes.data.server.id); + + if (server.data.server.status === 'running') { + break; + } else { + log.info(`Attempt ${attempts} Failed. Backing off for 30 seconds`); + } + } + + if (attempts === maxRetries) { + try { + throw new Error(`Failed to initialize server in hetzner after ${attempts} retries`); + } catch (error) { + log.error({message: error.message, stack: error.stack}); + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + text: `Failed to initialize server in Hetzner` + }); + return; + } + } + + maxRetries = 4; + for (attempts = 1; attempts <= maxRetries; attempts++) { + //wait 30 seconds + await delay(1000 * 30); + try { + //get servers and info from tailscale + const { deviceId } = await getDevices(serverName); + + await axios.post(`https://api.tailscale.com/api/v2/device/${deviceId}/tags`, + { + "tags": [ + `tag:${userEmail}` + ] + }, { + headers: { + 'Authorization': `Bearer ${process.env.TAILSCALE_API_TOKEN}`, + 'Content-Type': 'application/json' + } + }); + break; + } catch (error) { + log.info(`Attempt ${attempts} Failed. Backing off for 30 seconds`) + } + } + + if (attempts === maxRetries) { + try { + throw new Error(`Failed to set tags in tailscale after ${attempts} retries`); + } catch (error) { + log.error({message: error.message, stack: error.stack}); + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + text: `Failed to set tags in tailscale` + }); + return; + } + } + + //get servers and info from tailscale + const { deviceIP } = await getDevices(serverName); + + //return info for tailscale + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + text: `The server has been created: https://login.tailscale.com/admin/machines/${deviceIP}` + }); + }, + + //delete a hetzner server + deleteServer: async ({ app, body, serverName }) => { + //get server from hetzner + const data = await getServer(); + + //get servers from tailscale + const { deviceId } = await getDevices(serverName) + + + //get server id from hetzner + let vmid = null + for (const server of data.data.servers) { + if (server.name === serverName) { + vmid = server.id; + } + } + + //delete the device from tailscale + await axios.delete(`https://api.tailscale.com/api/v2/device/${deviceId}`, { + headers: { + 'Authorization': `Bearer ${process.env.TAILSCALE_API_TOKEN}` + } + }) + .catch(error => { + log.error('Failed to delete device in tailscale', axiosError(error)); + + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + text: `Failed to delete Server: ${serverName} from Tailscale` + }); + }); + + //delete server from hetzner + try { + await axios.delete(`https://api.hetzner.cloud/v1/servers/${vmid}`, { + headers: { + 'Authorization': `Bearer ${process.env.HETZNER_API_TOKEN}` + } + }); + + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + text: `Server: ${serverName} has been deleted.` + }); + } catch (error) { + log.error('Failed to delete the server in hetzner', axiosError(error)); + + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + text: `Failed to delete Server: ${serverName} from Hetzner.` + }); + } + }, + + //list the servers + listServers: async ({ app, body }) => { + const data = await getServer(); + + // Call the users.info method using the WebClient + const info = await app.client.users.info({ + user: body.user.id + }) + .catch(error => { + log.error('There was an error getting user.info from slack', error); + }); + + const userEmail = formatUser(info.user.profile.email); + + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + text: `Servers in Hetzner:` + }); + + //list the servers and build the buttons + for (const server of data.data.servers) { + if (server.labels.owner === userEmail) { + const { deviceIP } = await getDevices(server.name); + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + blocks: [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": `Server: ${server.name}\nServer id: ${server.id}\nStatus: ${server.status}\nConnect: https://login.tailscale.com/admin/machines/${deviceIP}` + } + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Start" + }, + "action_id": `button_start_hetzner_${server.id}` + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Stop" + }, + "action_id": `button_stop_hetzner_${server.id}` + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Delete" + }, + "action_id": `button_delete_hetzner_${server.name}` + } + ] + } + ], + text: "VM options" + }) + } + } + }, + + //start a hetzner server + startServer: async ({ app, body, vmid }) => { + try { + await axios.post(`https://api.hetzner.cloud/v1/servers/${vmid}/actions/poweron`, null, { + headers: { + 'Authorization': `Bearer ${process.env.HETZNER_API_TOKEN}` + } + }); + + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + text: `Server Id: ${vmid} has been started.` + }); + + } catch (error) { + log.error('Error starting the server', axiosError(error)); + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + text: `Server Id: ${vmid} failed to start.` + }); + + return; + } + }, + + //stop a hetzner server + stopServer: async ({ app, body, vmid }) => { + try { + await axios.post(`https://api.hetzner.cloud/v1/servers/${vmid}/actions/poweroff`, null, { + headers: { + 'Authorization': `Bearer ${process.env.HETZNER_API_TOKEN}` + } + }); + + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + text: `Server Id: ${vmid} has been stopped.` + }); + } catch (error) { + log.error('Error stopping the server', axiosError(error)); + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + text: `Server Id: ${vmid} failed to stop.` + }); + + return; + } + }, + + //code to build button UI + selectImage: async ({app, body }) => { + + //get the hetzner images + const images = await getHetznerImages(); + + //return if it fails to get the images. + if (!images) { + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + text: `Failed to get image data` + }); + + return; + } + + //build button for user to select + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + text: `Select an image:` + }); + for (const image of images) { + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + blocks: [ + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": `${image.description}` + }, + "action_id": `button_create_image_hetzner_${image.description}_${image.id}` + }, + ] + } + ], + text: "Select an image:" + }) + } + } +} \ No newline at end of file From 090327080b360d34929343614fef21509c2a0d09 Mon Sep 17 00:00:00 2001 From: NichArchA82 <64152648+NichArchA82@users.noreply.github.com> Date: Sun, 29 Sep 2024 15:39:53 +0000 Subject: [PATCH 2/2] feat: add aws --- command-handler/package-lock.json | 1315 +++++++++++++++++ command-handler/package.json | 1 + command-handler/src/cmd-handler/buttons.js | 9 +- command-handler/src/commands/vm.js | 95 +- command-handler/src/util/aws/aws-server.js | 397 +++++ .../src/util/aws/get-aws-images.js | 15 + command-handler/src/util/aws/get-instances.js | 37 + .../src/util/get-hetzner-images.js | 29 - command-handler/src/util/get-servers.js | 19 - command-handler/src/util/get-user-data.js | 11 + command-handler/src/util/hetzner-servers.js | 402 ----- 11 files changed, 1863 insertions(+), 467 deletions(-) create mode 100644 command-handler/src/util/aws/aws-server.js create mode 100644 command-handler/src/util/aws/get-aws-images.js create mode 100644 command-handler/src/util/aws/get-instances.js delete mode 100644 command-handler/src/util/get-hetzner-images.js delete mode 100644 command-handler/src/util/get-servers.js create mode 100644 command-handler/src/util/get-user-data.js delete mode 100644 command-handler/src/util/hetzner-servers.js diff --git a/command-handler/package-lock.json b/command-handler/package-lock.json index 3145c2a..92abd03 100644 --- a/command-handler/package-lock.json +++ b/command-handler/package-lock.json @@ -9,12 +9,707 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@aws-sdk/client-ec2": "^3.658.1", "axios": "^1.7.7", "dotenv": "^16.4.5", "unique-names-generator": "^4.7.1", "winston": "^3.14.2" } }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "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==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-ec2": { + "version": "3.658.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-ec2/-/client-ec2-3.658.1.tgz", + "integrity": "sha512-J/TdGg7Z8pwIL826QKwaX/EgND5Tst5N5hKcjwnj0jGfsJOkRTMdZTwOgvShYWgs6BplFFZqkl3t2dKsNfsVcg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.658.1", + "@aws-sdk/client-sts": "3.658.1", + "@aws-sdk/core": "3.658.1", + "@aws-sdk/credential-provider-node": "3.658.1", + "@aws-sdk/middleware-host-header": "3.654.0", + "@aws-sdk/middleware-logger": "3.654.0", + "@aws-sdk/middleware-recursion-detection": "3.654.0", + "@aws-sdk/middleware-sdk-ec2": "3.658.1", + "@aws-sdk/middleware-user-agent": "3.654.0", + "@aws-sdk/region-config-resolver": "3.654.0", + "@aws-sdk/types": "3.654.0", + "@aws-sdk/util-endpoints": "3.654.0", + "@aws-sdk/util-user-agent-browser": "3.654.0", + "@aws-sdk/util-user-agent-node": "3.654.0", + "@smithy/config-resolver": "^3.0.8", + "@smithy/core": "^2.4.6", + "@smithy/fetch-http-handler": "^3.2.8", + "@smithy/hash-node": "^3.0.6", + "@smithy/invalid-dependency": "^3.0.6", + "@smithy/middleware-content-length": "^3.0.8", + "@smithy/middleware-endpoint": "^3.1.3", + "@smithy/middleware-retry": "^3.0.21", + "@smithy/middleware-serde": "^3.0.6", + "@smithy/middleware-stack": "^3.0.6", + "@smithy/node-config-provider": "^3.1.7", + "@smithy/node-http-handler": "^3.2.3", + "@smithy/protocol-http": "^4.1.3", + "@smithy/smithy-client": "^3.3.5", + "@smithy/types": "^3.4.2", + "@smithy/url-parser": "^3.0.6", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.21", + "@smithy/util-defaults-mode-node": "^3.0.21", + "@smithy/util-endpoints": "^2.1.2", + "@smithy/util-middleware": "^3.0.6", + "@smithy/util-retry": "^3.0.6", + "@smithy/util-utf8": "^3.0.0", + "@smithy/util-waiter": "^3.1.5", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.658.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.658.1.tgz", + "integrity": "sha512-lOuaBtqPTYGn6xpXlQF4LsNDsQ8Ij2kOdnk+i69Kp6yS76TYvtUuukyLL5kx8zE1c8WbYtxj9y8VNw9/6uKl7Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.658.1", + "@aws-sdk/middleware-host-header": "3.654.0", + "@aws-sdk/middleware-logger": "3.654.0", + "@aws-sdk/middleware-recursion-detection": "3.654.0", + "@aws-sdk/middleware-user-agent": "3.654.0", + "@aws-sdk/region-config-resolver": "3.654.0", + "@aws-sdk/types": "3.654.0", + "@aws-sdk/util-endpoints": "3.654.0", + "@aws-sdk/util-user-agent-browser": "3.654.0", + "@aws-sdk/util-user-agent-node": "3.654.0", + "@smithy/config-resolver": "^3.0.8", + "@smithy/core": "^2.4.6", + "@smithy/fetch-http-handler": "^3.2.8", + "@smithy/hash-node": "^3.0.6", + "@smithy/invalid-dependency": "^3.0.6", + "@smithy/middleware-content-length": "^3.0.8", + "@smithy/middleware-endpoint": "^3.1.3", + "@smithy/middleware-retry": "^3.0.21", + "@smithy/middleware-serde": "^3.0.6", + "@smithy/middleware-stack": "^3.0.6", + "@smithy/node-config-provider": "^3.1.7", + "@smithy/node-http-handler": "^3.2.3", + "@smithy/protocol-http": "^4.1.3", + "@smithy/smithy-client": "^3.3.5", + "@smithy/types": "^3.4.2", + "@smithy/url-parser": "^3.0.6", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.21", + "@smithy/util-defaults-mode-node": "^3.0.21", + "@smithy/util-endpoints": "^2.1.2", + "@smithy/util-middleware": "^3.0.6", + "@smithy/util-retry": "^3.0.6", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.658.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.658.1.tgz", + "integrity": "sha512-RGcZAI3qEA05JszPKwa0cAyp8rnS1nUvs0Sqw4hqLNQ1kD7b7V6CPjRXe7EFQqCOMvM4kGqx0+cEEVTOmBsFLw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.658.1", + "@aws-sdk/credential-provider-node": "3.658.1", + "@aws-sdk/middleware-host-header": "3.654.0", + "@aws-sdk/middleware-logger": "3.654.0", + "@aws-sdk/middleware-recursion-detection": "3.654.0", + "@aws-sdk/middleware-user-agent": "3.654.0", + "@aws-sdk/region-config-resolver": "3.654.0", + "@aws-sdk/types": "3.654.0", + "@aws-sdk/util-endpoints": "3.654.0", + "@aws-sdk/util-user-agent-browser": "3.654.0", + "@aws-sdk/util-user-agent-node": "3.654.0", + "@smithy/config-resolver": "^3.0.8", + "@smithy/core": "^2.4.6", + "@smithy/fetch-http-handler": "^3.2.8", + "@smithy/hash-node": "^3.0.6", + "@smithy/invalid-dependency": "^3.0.6", + "@smithy/middleware-content-length": "^3.0.8", + "@smithy/middleware-endpoint": "^3.1.3", + "@smithy/middleware-retry": "^3.0.21", + "@smithy/middleware-serde": "^3.0.6", + "@smithy/middleware-stack": "^3.0.6", + "@smithy/node-config-provider": "^3.1.7", + "@smithy/node-http-handler": "^3.2.3", + "@smithy/protocol-http": "^4.1.3", + "@smithy/smithy-client": "^3.3.5", + "@smithy/types": "^3.4.2", + "@smithy/url-parser": "^3.0.6", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.21", + "@smithy/util-defaults-mode-node": "^3.0.21", + "@smithy/util-endpoints": "^2.1.2", + "@smithy/util-middleware": "^3.0.6", + "@smithy/util-retry": "^3.0.6", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.658.1" + } + }, + "node_modules/@aws-sdk/client-sts": { + "version": "3.658.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.658.1.tgz", + "integrity": "sha512-yw9hc5blTnbT1V6mR7Cx9HGc9KQpcLQ1QXj8rntiJi6tIYu3aFNVEyy81JHL7NsuBSeQulJTvHO3y6r3O0sfRg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.658.1", + "@aws-sdk/core": "3.658.1", + "@aws-sdk/credential-provider-node": "3.658.1", + "@aws-sdk/middleware-host-header": "3.654.0", + "@aws-sdk/middleware-logger": "3.654.0", + "@aws-sdk/middleware-recursion-detection": "3.654.0", + "@aws-sdk/middleware-user-agent": "3.654.0", + "@aws-sdk/region-config-resolver": "3.654.0", + "@aws-sdk/types": "3.654.0", + "@aws-sdk/util-endpoints": "3.654.0", + "@aws-sdk/util-user-agent-browser": "3.654.0", + "@aws-sdk/util-user-agent-node": "3.654.0", + "@smithy/config-resolver": "^3.0.8", + "@smithy/core": "^2.4.6", + "@smithy/fetch-http-handler": "^3.2.8", + "@smithy/hash-node": "^3.0.6", + "@smithy/invalid-dependency": "^3.0.6", + "@smithy/middleware-content-length": "^3.0.8", + "@smithy/middleware-endpoint": "^3.1.3", + "@smithy/middleware-retry": "^3.0.21", + "@smithy/middleware-serde": "^3.0.6", + "@smithy/middleware-stack": "^3.0.6", + "@smithy/node-config-provider": "^3.1.7", + "@smithy/node-http-handler": "^3.2.3", + "@smithy/protocol-http": "^4.1.3", + "@smithy/smithy-client": "^3.3.5", + "@smithy/types": "^3.4.2", + "@smithy/url-parser": "^3.0.6", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.21", + "@smithy/util-defaults-mode-node": "^3.0.21", + "@smithy/util-endpoints": "^2.1.2", + "@smithy/util-middleware": "^3.0.6", + "@smithy/util-retry": "^3.0.6", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.658.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.658.1.tgz", + "integrity": "sha512-vJVMoMcSKXK2gBRSu9Ywwv6wQ7tXH8VL1fqB1uVxgCqBZ3IHfqNn4zvpMPWrwgO2/3wv7XFyikGQ5ypPTCw4jA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^2.4.6", + "@smithy/node-config-provider": "^3.1.7", + "@smithy/property-provider": "^3.1.6", + "@smithy/protocol-http": "^4.1.3", + "@smithy/signature-v4": "^4.1.4", + "@smithy/smithy-client": "^3.3.5", + "@smithy/types": "^3.4.2", + "@smithy/util-middleware": "^3.0.6", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.654.0.tgz", + "integrity": "sha512-kogsx3Ql81JouHS7DkheCDU9MYAvK0AokxjcshDveGmf7BbgbWCA8Fnb9wjQyNDaOXNvkZu8Z8rgkX91z324/w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/property-provider": "^3.1.6", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.658.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.658.1.tgz", + "integrity": "sha512-4ubkJjEVCZflxkZnV1JDQv8P2pburxk1LrEp55telfJRzXrnowzBKwuV2ED0QMNC448g2B3VCaffS+Ct7c4IWQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/fetch-http-handler": "^3.2.8", + "@smithy/node-http-handler": "^3.2.3", + "@smithy/property-provider": "^3.1.6", + "@smithy/protocol-http": "^4.1.3", + "@smithy/smithy-client": "^3.3.5", + "@smithy/types": "^3.4.2", + "@smithy/util-stream": "^3.1.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.658.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.658.1.tgz", + "integrity": "sha512-2uwOamQg5ppwfegwen1ddPu5HM3/IBSnaGlaKLFhltkdtZ0jiqTZWUtX2V+4Q+buLnT0hQvLS/frQ+7QUam+0Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.654.0", + "@aws-sdk/credential-provider-http": "3.658.1", + "@aws-sdk/credential-provider-process": "3.654.0", + "@aws-sdk/credential-provider-sso": "3.658.1", + "@aws-sdk/credential-provider-web-identity": "3.654.0", + "@aws-sdk/types": "3.654.0", + "@smithy/credential-provider-imds": "^3.2.3", + "@smithy/property-provider": "^3.1.6", + "@smithy/shared-ini-file-loader": "^3.1.7", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.658.1" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.658.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.658.1.tgz", + "integrity": "sha512-XwxW6N+uPXPYAuyq+GfOEdfL/MZGAlCSfB5gEWtLBFmFbikhmEuqfWtI6CD60OwudCUOh6argd21BsJf8o1SJA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.654.0", + "@aws-sdk/credential-provider-http": "3.658.1", + "@aws-sdk/credential-provider-ini": "3.658.1", + "@aws-sdk/credential-provider-process": "3.654.0", + "@aws-sdk/credential-provider-sso": "3.658.1", + "@aws-sdk/credential-provider-web-identity": "3.654.0", + "@aws-sdk/types": "3.654.0", + "@smithy/credential-provider-imds": "^3.2.3", + "@smithy/property-provider": "^3.1.6", + "@smithy/shared-ini-file-loader": "^3.1.7", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.654.0.tgz", + "integrity": "sha512-PmQoo8sZ9Q2Ow8OMzK++Z9lI7MsRUG7sNq3E72DVA215dhtTICTDQwGlXH2AAmIp7n+G9LLRds+4wo2ehG4mkg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/property-provider": "^3.1.6", + "@smithy/shared-ini-file-loader": "^3.1.7", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.658.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.658.1.tgz", + "integrity": "sha512-YOagVEsZEk9DmgJEBg+4MBXrPcw/tYas0VQ5OVBqC5XHNbi2OBGJqgmjVPesuu393E7W0VQxtJFDS00O1ewQgA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.658.1", + "@aws-sdk/token-providers": "3.654.0", + "@aws-sdk/types": "3.654.0", + "@smithy/property-provider": "^3.1.6", + "@smithy/shared-ini-file-loader": "^3.1.7", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.654.0.tgz", + "integrity": "sha512-6a2g9gMtZToqSu+CusjNK5zvbLJahQ9di7buO3iXgbizXpLXU1rnawCpWxwslMpT5fLgMSKDnKDrr6wdEk7jSw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/property-provider": "^3.1.6", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.654.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.654.0.tgz", + "integrity": "sha512-rxGgVHWKp8U2ubMv+t+vlIk7QYUaRCHaVpmUlJv0Wv6Q0KeO9a42T9FxHphjOTlCGQOLcjCreL9CF8Qhtb4mdQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/protocol-http": "^4.1.3", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.654.0.tgz", + "integrity": "sha512-OQYb+nWlmASyXfRb989pwkJ9EVUMP1CrKn2eyTk3usl20JZmKo2Vjis6I0tLUkMSxMhnBJJlQKyWkRpD/u1FVg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.654.0.tgz", + "integrity": "sha512-gKSomgltKVmsT8sC6W7CrADZ4GHwX9epk3GcH6QhebVO3LA9LRbkL3TwOPUXakxxOLLUTYdOZLIOtFf7iH00lg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/protocol-http": "^4.1.3", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-ec2": { + "version": "3.658.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-ec2/-/middleware-sdk-ec2-3.658.1.tgz", + "integrity": "sha512-CnkMajiLD8c+PyiqMjdRt3n87oZnd8jw+8mbtB0jX7Q9ED2z+oeG+RTZMXp2QEiZ0Q+7RyKjXf/PLRhARppFog==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@aws-sdk/util-format-url": "3.654.0", + "@smithy/middleware-endpoint": "^3.1.3", + "@smithy/protocol-http": "^4.1.3", + "@smithy/signature-v4": "^4.1.4", + "@smithy/smithy-client": "^3.3.5", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.654.0.tgz", + "integrity": "sha512-liCcqPAyRsr53cy2tYu4qeH4MMN0eh9g6k56XzI5xd4SghXH5YWh4qOYAlQ8T66ZV4nPMtD8GLtLXGzsH8moFg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@aws-sdk/util-endpoints": "3.654.0", + "@smithy/protocol-http": "^4.1.3", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.654.0.tgz", + "integrity": "sha512-ydGOrXJxj3x0sJhsXyTmvJVLAE0xxuTWFJihTl67RtaO7VRNtd82I3P3bwoMMaDn5WpmV5mPo8fEUDRlBm3fPg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/node-config-provider": "^3.1.7", + "@smithy/types": "^3.4.2", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.654.0.tgz", + "integrity": "sha512-D8GeJYmvbfWkQDtTB4owmIobSMexZel0fOoetwvgCQ/7L8VPph3Q2bn1TRRIXvH7wdt6DcDxA3tKMHPBkT3GlA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/property-provider": "^3.1.6", + "@smithy/shared-ini-file-loader": "^3.1.7", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sso-oidc": "^3.654.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.654.0.tgz", + "integrity": "sha512-VWvbED3SV+10QJIcmU/PKjsKilsTV16d1I7/on4bvD/jo1qGeMXqLDBSen3ks/tuvXZF/mFc7ZW/W2DiLVtO7A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.654.0.tgz", + "integrity": "sha512-i902fcBknHs0Irgdpi62+QMvzxE+bczvILXigYrlHL4+PiEnlMVpni5L5W1qCkNZXf8AaMrSBuR1NZAGp6UOUw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/types": "^3.4.2", + "@smithy/util-endpoints": "^2.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/util-format-url": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.654.0.tgz", + "integrity": "sha512-2yAlJ/l1uTJhS52iu4+/EvdIyQhDBL+nATY8rEjFI0H+BHGVrJIH2CL4DByhvi2yvYwsqQX0HYah6pF/yoXukA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/querystring-builder": "^3.0.6", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.568.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.568.0.tgz", + "integrity": "sha512-3nh4TINkXYr+H41QaPelCceEB2FXP3fxp93YZXB/kqJvX0U9j0N0Uk45gvsjmEPzG8XxkPEeLIfT2I1M7A6Lig==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.654.0.tgz", + "integrity": "sha512-ykYAJqvnxLt7wfrqya28wuH3/7NdrwzfiFd7NqEVQf7dXVxL5RPEpD7DxjcyQo3DsHvvdUvGZVaQhozycn1pzA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/types": "^3.4.2", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.654.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.654.0.tgz", + "integrity": "sha512-a0ojjdBN6pqv6gB4H/QPPSfhs7mFtlVwnmKCM/QrTaFzN0U810PJ1BST3lBx5sa23I5jWHGaoFY+5q65C3clLQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.654.0", + "@smithy/node-config-provider": "^3.1.7", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, "node_modules/@colors/colors": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", @@ -35,6 +730,573 @@ "kuler": "^2.0.0" } }, + "node_modules/@smithy/abort-controller": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.4.tgz", + "integrity": "sha512-VupaALAQlXViW3/enTf/f5l5JZYSAxoJL7f0nanhNNKnww6DGCg1oYIuNP78KDugnkwthBO6iEcym16HhWV8RQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-3.0.8.tgz", + "integrity": "sha512-Tv1obAC18XOd2OnDAjSWmmthzx6Pdeh63FbLin8MlPiuJ2ATpKkq0NcNOJFr0dO+JmZXnwu8FQxKJ3TKJ3Hulw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^3.1.7", + "@smithy/types": "^3.4.2", + "@smithy/util-config-provider": "^3.0.0", + "@smithy/util-middleware": "^3.0.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-2.4.6.tgz", + "integrity": "sha512-6lQQp99hnyuNNIzeTYSzCUXJHwvvFLY7hfdFGSJM95tjRDJGfzWYFRBXPaM9766LiiTsQ561KErtbufzUFSYUg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-endpoint": "^3.1.3", + "@smithy/middleware-retry": "^3.0.21", + "@smithy/middleware-serde": "^3.0.6", + "@smithy/protocol-http": "^4.1.3", + "@smithy/smithy-client": "^3.3.5", + "@smithy/types": "^3.4.2", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-middleware": "^3.0.6", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-3.2.3.tgz", + "integrity": "sha512-VoxMzSzdvkkjMJNE38yQgx4CfnmT+Z+5EUXkg4x7yag93eQkVQgZvN3XBSHC/ylfBbLbAtdu7flTCChX9I+mVg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^3.1.7", + "@smithy/property-provider": "^3.1.6", + "@smithy/types": "^3.4.2", + "@smithy/url-parser": "^3.0.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-3.2.8.tgz", + "integrity": "sha512-Lqe0B8F5RM7zkw//6avq1SJ8AfaRd3ubFUS1eVp5WszV7p6Ne5hQ4dSuMHDpNRPhgTvj4va9Kd/pcVigHEHRow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^4.1.3", + "@smithy/querystring-builder": "^3.0.6", + "@smithy/types": "^3.4.2", + "@smithy/util-base64": "^3.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/hash-node": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-3.0.6.tgz", + "integrity": "sha512-c/FHEdKK/7DU2z6ZE91L36ahyXWayR3B+FzELjnYq7wH5YqIseM24V+pWCS9kFn1Ln8OFGTf+pyYPiHZuX0s/Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.4.2", + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-3.0.6.tgz", + "integrity": "sha512-czM7Ioq3s8pIXht7oD+vmgy4Wfb4XavU/k/irO8NdXFFOx7YAlsCCcKOh/lJD1mJSYQqiR7NmpZ9JviryD/7AQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-3.0.0.tgz", + "integrity": "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-3.0.8.tgz", + "integrity": "sha512-VuyszlSO49WKh3H9/kIO2kf07VUwGV80QRiaDxUfP8P8UKlokz381ETJvwLhwuypBYhLymCYyNhB3fLAGBX2og==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^4.1.3", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-3.1.3.tgz", + "integrity": "sha512-KeM/OrK8MVFUsoJsmCN0MZMVPjKKLudn13xpgwIMpGTYpA8QZB2Xq5tJ+RE6iu3A6NhOI4VajDTwBsm8pwwrhg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^3.0.6", + "@smithy/node-config-provider": "^3.1.7", + "@smithy/shared-ini-file-loader": "^3.1.7", + "@smithy/types": "^3.4.2", + "@smithy/url-parser": "^3.0.6", + "@smithy/util-middleware": "^3.0.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.21.tgz", + "integrity": "sha512-/h0fElV95LekVVEJuSw+aI11S1Y3zIUwBc6h9ZbUv43Gl2weXsbQwjLoet6j/Qtb0phfrSxS6pNg6FqgJOWZkA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^3.1.7", + "@smithy/protocol-http": "^4.1.3", + "@smithy/service-error-classification": "^3.0.6", + "@smithy/smithy-client": "^3.3.5", + "@smithy/types": "^3.4.2", + "@smithy/util-middleware": "^3.0.6", + "@smithy/util-retry": "^3.0.6", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-3.0.6.tgz", + "integrity": "sha512-KKTUSl1MzOM0MAjGbudeaVNtIDo+PpekTBkCNwvfZlKndodrnvRo+00USatiyLOc0ujjO9UydMRu3O9dYML7ag==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-3.0.6.tgz", + "integrity": "sha512-2c0eSYhTQ8xQqHMcRxLMpadFbTXg6Zla5l0mwNftFCZMQmuhI7EbAJMx6R5eqfuV3YbJ3QGyS3d5uSmrHV8Khg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.7.tgz", + "integrity": "sha512-g3mfnC3Oo8pOI0dYuPXLtdW1WGVb3bR2tkV21GNkm0ZvQjLTtamXAwCWt/FCb0HGvKt3gHHmF1XerG0ICfalOg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^3.1.6", + "@smithy/shared-ini-file-loader": "^3.1.7", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.2.3.tgz", + "integrity": "sha512-/gcm5DJ3k1b1zEInzBGAZC8ntJ+jwrz1NcSIu+9dSXd1FfG0G6QgkDI40tt8/WYUbHtLyo8fEqtm2v29koWo/w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^3.1.4", + "@smithy/protocol-http": "^4.1.3", + "@smithy/querystring-builder": "^3.0.6", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-3.1.6.tgz", + "integrity": "sha512-NK3y/T7Q/Bw+Z8vsVs9MYIQ5v7gOX7clyrXcwhhIBQhbPgRl6JDrZbusO9qWDhcEus75Tg+VCxtIRfo3H76fpw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.1.3.tgz", + "integrity": "sha512-GcbMmOYpH9iRqtC05RbRnc/0FssxSTHlmaNhYBTgSgNCYpdR3Kt88u5GAZTBmouzv+Zlj/VRv92J9ruuDeJuEw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.6.tgz", + "integrity": "sha512-sQe08RunoObe+Usujn9+R2zrLuQERi3CWvRO3BvnoWSYUaIrLKuAIeY7cMeDax6xGyfIP3x/yFWbEKSXvOnvVg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.4.2", + "@smithy/util-uri-escape": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-3.0.6.tgz", + "integrity": "sha512-UJKw4LlEkytzz2Wq+uIdHf6qOtFfee/o7ruH0jF5I6UAuU+19r9QV7nU3P/uI0l6+oElRHmG/5cBBcGJrD7Ozg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-3.0.6.tgz", + "integrity": "sha512-53SpchU3+DUZrN7J6sBx9tBiCVGzsib2e4sc512Q7K9fpC5zkJKs6Z9s+qbMxSYrkEkle6hnMtrts7XNkMJJMg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.4.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.7.tgz", + "integrity": "sha512-IA4K2qTJYXkF5OfVN4vsY1hfnUZjaslEE8Fsr/gGFza4TAC2A9NfnZuSY2srQIbt9bwtjHiAayrRVgKse4Q7fA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-4.1.4.tgz", + "integrity": "sha512-72MiK7xYukNsnLJI9NqvUHqTu0ziEsfMsYNlWpiJfuGQnCTFKpckThlEatirvcA/LmT1h7rRO+pJD06PYsPu9Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "@smithy/protocol-http": "^4.1.3", + "@smithy/types": "^3.4.2", + "@smithy/util-hex-encoding": "^3.0.0", + "@smithy/util-middleware": "^3.0.6", + "@smithy/util-uri-escape": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-3.3.5.tgz", + "integrity": "sha512-7IZi8J3Dr9n3tX+lcpmJ/5tCYIqoXdblFBaPuv0SEKZFRpCxE+TqIWL6I3t7jLlk9TWu3JSvEZAhtjB9yvB+zA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-endpoint": "^3.1.3", + "@smithy/middleware-stack": "^3.0.6", + "@smithy/protocol-http": "^4.1.3", + "@smithy/types": "^3.4.2", + "@smithy/util-stream": "^3.1.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-3.4.2.tgz", + "integrity": "sha512-tHiFcfcVedVBHpmHUEUHOCCih8iZbIAYn9NvPsNzaPm/237I3imdDdZoOC8c87H5HBAVEa06tTgb+OcSWV9g5w==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-3.0.6.tgz", + "integrity": "sha512-47Op/NU8Opt49KyGpHtVdnmmJMsp2hEwBdyjuFB9M2V5QVOwA7pBhhxKN5z6ztKGrMw76gd8MlbPuzzvaAncuQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^3.0.6", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/util-base64": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-3.0.0.tgz", + "integrity": "sha512-Kxvoh5Qtt0CDsfajiZOCpJxgtPHXOKwmM+Zy4waD43UoEMA+qPxxa98aE/7ZhdnBFZFXMOiBR5xbcaMhLtznQQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-3.0.0.tgz", + "integrity": "sha512-cbjJs2A1mLYmqmyVl80uoLTJhAcfzMOyPgjwAYusWKMdLeNtzmMz9YxNl3/jRLoxSS3wkqkf0jwNdtXWtyEBaQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-3.0.0.tgz", + "integrity": "sha512-Tj7pZ4bUloNUP6PzwhN7K386tmSmEET9QtQg0TgdNOnxhZvCssHji+oZTUIuzxECRfG8rdm2PMw2WCFs6eIYkA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz", + "integrity": "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-3.0.0.tgz", + "integrity": "sha512-pbjk4s0fwq3Di/ANL+rCvJMKM5bzAQdE5S/6RL5NXgMExFAi6UgQMPOm5yPaIWPpr+EOXKXRonJ3FoxKf4mCJQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.21.tgz", + "integrity": "sha512-M/FhTBk4c/SsB91dD/M4gMGfJO7z/qJaM9+XQQIqBOf4qzZYMExnP7R4VdGwxxH8IKMGW+8F0I4rNtVRrcfPoA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^3.1.6", + "@smithy/smithy-client": "^3.3.5", + "@smithy/types": "^3.4.2", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.21.tgz", + "integrity": "sha512-NiLinPvF86U3S2Pdx/ycqd4bnY5dmFSPNL5KYRwbNjqQFS09M5Wzqk8BNk61/47xCYz1X/6KeiSk9qgYPTtuDw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^3.0.8", + "@smithy/credential-provider-imds": "^3.2.3", + "@smithy/node-config-provider": "^3.1.7", + "@smithy/property-provider": "^3.1.6", + "@smithy/smithy-client": "^3.3.5", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-2.1.2.tgz", + "integrity": "sha512-FEISzffb4H8DLzGq1g4MuDpcv6CIG15fXoQzDH9SjpRJv6h7J++1STFWWinilG0tQh9H1v2UKWG19Jjr2B16zQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^3.1.7", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-3.0.0.tgz", + "integrity": "sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-3.0.6.tgz", + "integrity": "sha512-BxbX4aBhI1O9p87/xM+zWy0GzT3CEVcXFPBRDoHAM+pV0eSW156pR+PSYEz0DQHDMYDsYAflC2bQNz2uaDBUZQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-3.0.6.tgz", + "integrity": "sha512-BRZiuF7IwDntAbevqMco67an0Sr9oLQJqqRCsSPZZHYRnehS0LHDAkJk/pSmI7Z8c/1Vet294H7fY2fWUgB+Rg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^3.0.6", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-3.1.8.tgz", + "integrity": "sha512-hoKOqSmb8FD3WLObuB5hwbM7bNIWgcnvkThokTvVq7J5PKjlLUK5qQQcB9zWLHIoSaIlf3VIv2OxZY2wtQjcRQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^3.2.8", + "@smithy/node-http-handler": "^3.2.3", + "@smithy/types": "^3.4.2", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-buffer-from": "^3.0.0", + "@smithy/util-hex-encoding": "^3.0.0", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-3.0.0.tgz", + "integrity": "sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", + "integrity": "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-3.1.5.tgz", + "integrity": "sha512-jYOSvM3H6sZe3CHjzD2VQNCjWBJs+4DbtwBMvUp9y5EnnwNa7NQxTeYeQw0CKCAdGGZ3QvVkyJmvbvs5M/B10A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^3.1.4", + "@smithy/types": "^3.4.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@types/triple-beam": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", @@ -78,6 +1340,12 @@ "node": ">= 6" } }, + "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==", + "license": "MIT" + }, "node_modules/color": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", @@ -162,6 +1430,28 @@ "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", "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" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", @@ -353,6 +1643,12 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "license": "MIT" + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -368,6 +1664,12 @@ "node": ">= 14.0.0" } }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD" + }, "node_modules/unique-names-generator": { "version": "4.7.1", "resolved": "https://registry.npmjs.org/unique-names-generator/-/unique-names-generator-4.7.1.tgz", @@ -383,6 +1685,19 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "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" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/winston": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/winston/-/winston-3.14.2.tgz", diff --git a/command-handler/package.json b/command-handler/package.json index a68398c..a8296bb 100644 --- a/command-handler/package.json +++ b/command-handler/package.json @@ -11,6 +11,7 @@ "license": "ISC", "description": "", "dependencies": { + "@aws-sdk/client-ec2": "^3.658.1", "axios": "^1.7.7", "dotenv": "^16.4.5", "unique-names-generator": "^4.7.1", diff --git a/command-handler/src/cmd-handler/buttons.js b/command-handler/src/cmd-handler/buttons.js index 8f6c2ea..56b76a5 100644 --- a/command-handler/src/cmd-handler/buttons.js +++ b/command-handler/src/cmd-handler/buttons.js @@ -2,9 +2,6 @@ const registeredButtons = { button_list_servers: { command: 'vm', }, - button_create_vm: { - command: 'vm', - }, button_delete_vm: { command: 'vm', }, @@ -27,7 +24,11 @@ const registeredButtons = { "^button_create_image_": { command: 'vm', isRegex: true, - } + }, + "^button_create_vm": { + command: 'vm', + isRegex: true, + }, }; export default registeredButtons; \ No newline at end of file diff --git a/command-handler/src/commands/vm.js b/command-handler/src/commands/vm.js index 8773248..4111413 100644 --- a/command-handler/src/commands/vm.js +++ b/command-handler/src/commands/vm.js @@ -1,4 +1,5 @@ -import hetzner from '../util/hetzner-servers.js' +import hetzner from '../util/hetzner/hetzner-servers.js'; +import aws from '../util/aws/aws-server.js'; export default { description: 'Sets up vm options', @@ -6,33 +7,101 @@ export default { button: async ({ app, actionId, body, response }) => { if (actionId === 'button_list_servers') { //list hetzner servers - hetzner.listServers({ app, body }) + + await aws.listServers({ app, body }); + await hetzner.listServers({ app, body }) } else if (actionId === 'button_create_vm') { - //select the hetzner server to create before calling the create server - hetzner.selectImage({ app, body }); + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + blocks: [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Select a platform to create the vm in:" + } + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "aws" + }, + "action_id": "button_create_vm_aws" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "hetzner" + }, + "action_id": "button_create_vm_hetzner" + } + ] + } + ], + text: "VM options" + }) - } else if (actionId.startsWith('button_start')) { - const vmid = actionId.split('_')[2]; + } else if (actionId.startsWith('button_start_aws')) { + const instanceId = actionId.split('_')[3]; + + //start a hetzner server + aws.startServer({ app, body, instanceId }); + + } else if (actionId.startsWith('button_stop_aws')) { + const instanceId = actionId.split('_')[3]; + + //stop a hetzner server + aws.stopServer({ app, body, instanceId }); + + } else if (actionId.startsWith('button_delete_aws')) { + const instanceId = actionId.split('_')[3]; + const serverName = actionId.split('_')[4]; + + //delete the server + aws.deleteServer({ app, body, instanceId, serverName }) + + } else if (actionId.startsWith('button_start_hetzner')) { + const vmid = actionId.split('_')[3]; //start a hetzner server hetzner.startServer({ app, body, vmid }); - } else if (actionId.startsWith('button_stop')) { - const vmid = actionId.split('_')[2]; + } else if (actionId.startsWith('button_stop_hetzner')) { + const vmid = actionId.split('_')[3]; //stop a hetzner server hetzner.stopServer({ app, body, vmid }); - } else if (actionId.startsWith('button_delete')) { - const serverName = actionId.split('_')[2]; + } else if (actionId.startsWith('button_delete_hetzner')) { + const serverName = actionId.split('_')[3]; //delete the server hetzner.deleteServer({app, body, serverName}) - } else if (actionId.startsWith('button_create_image')) { - const imageName = actionId.split('_')[3]; - const imageID = actionId.split('_')[4]; + } else if (actionId.startsWith('button_create_image_aws')) { + const imageName = actionId.split('_')[4]; + const ami = actionId.split('_')[5]; + aws.createServer({ app, body, imageName, ami }); + + } else if (actionId.startsWith('button_create_image_hetzner')) { + const imageName = actionId.split('_')[4]; + const imageID = actionId.split('_')[5]; hetzner.createServer({ app, body, imageID, imageName }); + + } else if (actionId.startsWith('button_create_vm_hetzner')) { + //select the hetzner server to create before calling the create server + hetzner.selectImage({ app, body }); + + } else if (actionId.startsWith('button_create_vm_aws')) { + //select the asw server to create before calling the create server + aws.selectImage({ app, body }); + } else { response({ text: `This button is registered with the vm command, but does not have an action associated with it.` diff --git a/command-handler/src/util/aws/aws-server.js b/command-handler/src/util/aws/aws-server.js new file mode 100644 index 0000000..ab22d08 --- /dev/null +++ b/command-handler/src/util/aws/aws-server.js @@ -0,0 +1,397 @@ +import { + DescribeSecurityGroupsCommand, + EC2Client, RunInstancesCommand, + StopInstancesCommand, + StartInstancesCommand, + TerminateInstancesCommand +} from "@aws-sdk/client-ec2"; +import logger from '../logger.js'; +import axios from 'axios'; +import 'dotenv/config'; +import formatUser from '../format-user.js'; +import getDevices from '../get-devices-info.js'; +import getInstance from "./get-instances.js"; +import configUserData from "../get-user-data.js"; +import getAwsImages from "./get-aws-images.js"; +import axiosError from '../axios-error-handler.js'; +import { uniqueNamesGenerator, colors, animals } from 'unique-names-generator'; + +const log = logger(); + +const delay = (ms) => { + return new Promise(resolve => setTimeout(resolve, ms)); + } + +const instanceType = 't3a.large'; + +export default { + getSecurityGroups: async({ app, body }) => { + const client = new EC2Client(); + try { + const { SecurityGroups } = await client.send( + new DescribeSecurityGroupsCommand({}), + ); + + const securityGroupList = SecurityGroups.slice(0, 9) + .map((sg) => ` • ${sg.GroupId}: ${sg.GroupName}`) + .join("\n"); + + console.log( + "Hello, Amazon EC2! Let's list up to 10 of your security groups:", + ); + console.log(securityGroupList); + } catch (err) { + console.error(err); + } + }, + + createServer: async({ app, body, imageName, ami }) => { + //auto generate the name + const serverName = uniqueNamesGenerator({ + dictionaries: [ colors, animals ], + separator: '-' + }); + + // Call the users.info method using the WebClient + let info; + try { + info = await app.client.users.info({ + user: body.user.id + }); + } catch (error) { + log.error('There was an error calling the user.info method in slack', error); + + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + text: `Failed to get user info from slack` + }); + + return; + } + + const userEmail = formatUser(info.user.profile.email); + + //post a status message + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + text: `Creating the server with image: ${imageName} This will take about 5 minutes.` + }); + // This example launches an instance using the specified AMI, instance type, security group, subnet, block device mapping, and tags. + const client = new EC2Client(); + const input = { + "BlockDeviceMappings": [ + { + "DeviceName": "/dev/xvda", + "Ebs": { + "VolumeSize": 52, + "VolumeType": "gp3" + } + } + ], + "ImageId": ami, + "InstanceType": instanceType, + // "KeyName": "my-key-pair", + "MaxCount": 1, + "MinCount": 1, + "SecurityGroupIds": [ + "sg-0b5b0e1fb1f73dbba" + ], + // "SubnetId": "subnet-6e7f829e", + "TagSpecifications": [ + { + "ResourceType": "instance", + "Tags": [ + { + "Key": "Name", + "Value": serverName + }, + { + "Key": "Owner", + "Value": userEmail + }, + ] + } + ], + "UserData": Buffer.from(configUserData(serverName)).toString('base64'), + }; + const command = new RunInstancesCommand(input); + await client.send(command); + + let maxRetries = 20; + let attempts; + + for (attempts = 1; attempts <= maxRetries; attempts++) { + //wait 30 seconds + await delay(1000 * 30); + + const server = await getInstance({ serverName }); + + if (server[0].State.Name === 'running') { + break; + } else { + log.info(`Attempt ${attempts} Failed. Backing off for 30 seconds`); + } + break; + } + + if (attempts === maxRetries) { + try { + throw new Error(`Failed to initialize server in aws after ${attempts} retries`); + } catch (error) { + log.error({message: error.message, stack: error.stack}); + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + text: `Failed to initialize server in aws` + }); + return; + } + } + + maxRetries = 4; + for (attempts = 1; attempts <= maxRetries; attempts++) { + //wait 30 seconds + await delay(1000 * 30); + try { + //get servers and info from tailscale + const { deviceId } = await getDevices(serverName); + + await axios.post(`https://api.tailscale.com/api/v2/device/${deviceId}/tags`, + { + "tags": [ + `tag:${userEmail}` + ] + }, { + headers: { + 'Authorization': `Bearer ${process.env.TAILSCALE_API_TOKEN}`, + 'Content-Type': 'application/json' + } + }); + break; + } catch (error) { + log.info(`Attempt ${attempts} Failed. Backing off for 30 seconds`) + } + } + + if (attempts === maxRetries) { + try { + throw new Error(`Failed to set tags in tailscale after ${attempts} retries`); + } catch (error) { + log.error({message: error.message, stack: error.stack}); + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + text: `Failed to set tags in tailscale` + }); + return; + } + } + + //get servers and info from tailscale + const { deviceIP } = await getDevices(serverName); + + //return info for tailscale + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + text: `The server has been created: https://login.tailscale.com/admin/machines/${deviceIP}` + }); + }, + + deleteServer: async ({ app, body, instanceId, serverName }) => { + //get servers from tailscale + const { deviceId } = await getDevices(serverName) + + //delete the device from tailscale + await axios.delete(`https://api.tailscale.com/api/v2/device/${deviceId}`, { + headers: { + 'Authorization': `Bearer ${process.env.TAILSCALE_API_TOKEN}` + } + }) + .catch(error => { + log.error('Failed to delete device in tailscale', axiosError(error)); + + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + text: `Failed to delete Server: ${serverName} from Tailscale` + }); + }); + + //terminate the server from aws + const client = new EC2Client(); + const command = new TerminateInstancesCommand({ + InstanceIds: [instanceId], + }); + try { + await client.send(command); + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + text: `Server: ${serverName} has been deleted.` + }); + } catch (error) { + log.error('Failed to delete the server in aws', {errorStack: error.stack, message: error.message}); + + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + text: `Failed to delete Server: ${serverName} from aws.` + }); + } + }, + + listServers: async({ app, body }) => { + // Call the users.info method using the WebClient + const info = await app.client.users.info({ + user: body.user.id + }) + .catch(error => { + log.error('There was an error getting user.info from slack', error); + }); + + const userEmail = formatUser(info.user.profile.email); + + //get the instances from aws + const instances = await getInstance({ userEmail }); + + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + text: `Servers in AWS:` + }); + + // list the servers and build the buttons + for (const instance of instances) { + //get the tag name from the instance + const serverName = instance.Tags.find(tag => tag.Key === 'Name')?.Value; + const { deviceIP } = await getDevices(serverName); + + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + blocks: [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": `Server: ${serverName}\nServer id: ${instance.InstanceId}\nStatus: ${instance.State.Name}\nConnect: https://login.tailscale.com/admin/machines/${deviceIP}` + } + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Start" + }, + "action_id": `button_start_aws_${instance.InstanceId}` + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Stop" + }, + "action_id": `button_stop_aws_${instance.InstanceId}` + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Delete" + }, + "action_id": `button_delete_aws_${instance.InstanceId}_${serverName}` + } + ] + } + ], + text: "VM options" + }) + } + // example id: to-describe-an-amazon-ec2-instance-1529025982172 + }, + + startServer: async({ app, body, instanceId }) => { + const client = new EC2Client(); + const command = new StartInstancesCommand({ + InstanceIds: [instanceId], + }); + try { + await client.send(command); + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + text: `Server Id: ${instanceId} has been started.` + }); + } catch (error) { + log.error('error starting aws server', {errorStack: error.stack, message: error.message}); + } + }, + + stopServer: async({ app, body, instanceId }) => { + const client = new EC2Client(); + const command = new StopInstancesCommand({ + InstanceIds: [instanceId], + }); + try { + await client.send(command); + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + text: `Server Id: ${instanceId} has been stopped.` + }); + } catch (error) { + log.error('error stoping aws server', {errorStack: error.stack, message: error.message}); + } + }, + + selectImage: async({ app, body }) => { + //get the aws images + const images = await getAwsImages(); + + //return if it fails to get the images. + if (!images) { + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + text: `Failed to get image data` + }); + + return; + } + + //build button for user to select + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + text: `Select an image:` + }); + for (const image of images) { + app.client.chat.postEphemeral({ + channel: `${body.channel.id}`, + user: `${body.user.id}`, + blocks: [ + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": `${image.Name}` + }, + "action_id": `button_create_image_aws_${image.Name}_${image.ImageId}` + }, + ] + } + ], + text: "Select an image:" + }); + } + } +}; \ No newline at end of file diff --git a/command-handler/src/util/aws/get-aws-images.js b/command-handler/src/util/aws/get-aws-images.js new file mode 100644 index 0000000..d858318 --- /dev/null +++ b/command-handler/src/util/aws/get-aws-images.js @@ -0,0 +1,15 @@ +import { EC2Client, DescribeImagesCommand } from "@aws-sdk/client-ec2"; + +export default async function getAwsImages() { + const client = new EC2Client(); + const input = { + Owners: ['self'] + }; + + const command = new DescribeImagesCommand(input); + const response = await client.send(command); + + return response.Images + .sort((a, b) => new Date(b.CreationDate) - new Date(a.CreationDate)) + .slice(0, 5); // Get only the latest 5 +} \ No newline at end of file diff --git a/command-handler/src/util/aws/get-instances.js b/command-handler/src/util/aws/get-instances.js new file mode 100644 index 0000000..a296646 --- /dev/null +++ b/command-handler/src/util/aws/get-instances.js @@ -0,0 +1,37 @@ +import { DescribeInstancesCommand, EC2Client } from "@aws-sdk/client-ec2"; + +export default async function getInstance({ userEmail = "", serverName = "" } = {}) { + // This example describes the specified instance. + const client = new EC2Client(); + + const filters = [ + { + Name: "instance-state-name", // Filter by instance state + Values: ["pending", "running", "shutting-down", "stopping", "stopped"] + } + ]; + + if (userEmail) { + filters.push({ + Name: "tag:Owner", + Values: [userEmail] + }); + } else if (serverName) { + filters.push({ + Name: "tag:Name", + Values: [serverName] + }); + } + + const input = { + Filters: filters, + // "InstanceIds": [ + // "i-1234567890abcdef0" + // ] + }; + const command = new DescribeInstancesCommand(input); + const response = await client.send(command); + const instances = response.Reservations.flatMap(reservation => reservation.Instances); + + return instances; +} \ No newline at end of file diff --git a/command-handler/src/util/get-hetzner-images.js b/command-handler/src/util/get-hetzner-images.js deleted file mode 100644 index 85cdb1e..0000000 --- a/command-handler/src/util/get-hetzner-images.js +++ /dev/null @@ -1,29 +0,0 @@ -import axios from "axios" -import logger from "./logger.js" -import axiosError from "./axios-error-handler.js"; - -const log = logger(); - -export default async function getHetznerImages() { - const response = await axios.get(`https://api.hetzner.cloud/v1/images?per_page=5&page=1&sort=created:desc&type=snapshot`, { - headers: { - 'Authorization': `Bearer ${process.env.HETZNER_API_TOKEN}` - } - }) - .catch(error => { - log.error('Failed to get images from hetzner', axiosError(error)); - }); - - const images = response.data.images; - - try { - if (!Array.isArray(images)) { - throw new Error('images is not an array'); - } - } catch (error) { - log.error({message: error.message, stack: error.stack}); - return null; - } - - return images; -} \ No newline at end of file diff --git a/command-handler/src/util/get-servers.js b/command-handler/src/util/get-servers.js deleted file mode 100644 index 099634d..0000000 --- a/command-handler/src/util/get-servers.js +++ /dev/null @@ -1,19 +0,0 @@ -import 'dotenv/config'; -import axios from 'axios'; -import axiosError from './axios-error-handler.js'; -import logger from './logger.js'; - -const log = logger(); - -export default async function getServer(serverId = "") { - const data = await axios.get(`https://api.hetzner.cloud/v1/servers/${serverId}`, { - headers: { - 'Authorization': `Bearer ${process.env.HETZNER_API_TOKEN}` - } - }) - .catch(error => { - log.error('Failed to get servers from hetzner', axiosError(error)); - }); - - return data; -} \ No newline at end of file diff --git a/command-handler/src/util/get-user-data.js b/command-handler/src/util/get-user-data.js new file mode 100644 index 0000000..5ec25f2 --- /dev/null +++ b/command-handler/src/util/get-user-data.js @@ -0,0 +1,11 @@ +export default function configUserData(serverName) { + const userData = ` + #cloud-config + runcmd: + - ['tailscale', 'up', '--authkey=${process.env.TAILSCALE_AUTH_KEY}', '--hostname=${serverName}'] + - ['tailscale', 'set', '--ssh'] + - ['tailscale', 'set', '--accept-routes'] + - ['passwd', '-d', 'root'] + ` + return userData; +} \ No newline at end of file diff --git a/command-handler/src/util/hetzner-servers.js b/command-handler/src/util/hetzner-servers.js deleted file mode 100644 index c0cfbd2..0000000 --- a/command-handler/src/util/hetzner-servers.js +++ /dev/null @@ -1,402 +0,0 @@ -import logger from './logger.js'; -import axios from 'axios'; -import 'dotenv/config'; -import formatUser from './format-user.js'; -import getDevices from './get-devices-info.js'; -import getServer from './get-servers.js'; -import axiosError from './axios-error-handler.js'; -import getHetznerImages from './get-hetzner-images.js'; -import { uniqueNamesGenerator, colors, animals } from 'unique-names-generator'; - -const log = logger(); - -const delay = (ms) => { - return new Promise(resolve => setTimeout(resolve, ms)); - } - -const region = "hel1"; -const serverType = 'cx42'; -const userData = ` -#cloud-config -runcmd: - - ['tailscale', 'up', '--authkey=${process.env.TAILSCALE_AUTH_KEY}'] - - ['tailscale', 'set', '--ssh'] - - ['tailscale', 'set', '--accept-routes'] - - ['passwd', '-d', 'root'] -` - -export default { - //create the hetzner server - createServer: async ({ app, body, imageID, imageName }) => { - //auto generate the name - const serverName = uniqueNamesGenerator({ - dictionaries: [ colors, animals ], - separator: '-' - }); - - // Call the users.info method using the WebClient - let info; - try { - info = await app.client.users.info({ - user: body.user.id - }); - } catch (error) { - log.error('There was an error calling the user.info method in slack', error); - - app.client.chat.postEphemeral({ - channel: `${body.channel.id}`, - user: `${body.user.id}`, - text: `Failed to get user info from slack` - }); - - return; - } - - const userEmail = formatUser(info.user.profile.email); - - //post a status message - app.client.chat.postEphemeral({ - channel: `${body.channel.id}`, - user: `${body.user.id}`, - text: `Creating the server with image: ${imageName} This will take about 5 minutes.` - }); - - //hetzner api to create the server - let serverRes; - try { - serverRes = await axios.post('https://api.hetzner.cloud/v1/servers', - { - "automount": false, - "image": imageID, - "labels": { - "owner": userEmail - }, - "location": region, - "name": serverName, - "public_net": { - "enable_ipv4": true, - "enable_ipv6": false - }, - "server_type": serverType, - "start_after_create": true, - "user_data": userData - }, { - headers: { - 'Authorization': `Bearer ${process.env.HETZNER_API_TOKEN}`, - 'Content-Type': 'application/json' - } - }); - } catch (error) { - log.error('There was an error creating the server', axiosError(error)); - - app.client.chat.postEphemeral({ - channel: `${body.channel.id}`, - user: `${body.user.id}`, - text: `Failed to create a server in hetzner.` - }); - - return; - } - - let maxRetries = 20; - let attempts; - - for (attempts = 1; attempts <= maxRetries; attempts++) { - //wait 30 seconds - await delay(1000 * 30); - - const server = await getServer(serverRes.data.server.id); - - if (server.data.server.status === 'running') { - break; - } else { - log.info(`Attempt ${attempts} Failed. Backing off for 30 seconds`); - } - } - - if (attempts === maxRetries) { - try { - throw new Error(`Failed to initialize server in hetzner after ${attempts} retries`); - } catch (error) { - log.error({message: error.message, stack: error.stack}); - app.client.chat.postEphemeral({ - channel: `${body.channel.id}`, - user: `${body.user.id}`, - text: `Failed to initialize server in Hetzner` - }); - return; - } - } - - maxRetries = 4; - for (attempts = 1; attempts <= maxRetries; attempts++) { - //wait 30 seconds - await delay(1000 * 30); - try { - //get servers and info from tailscale - const { deviceId } = await getDevices(serverName); - - await axios.post(`https://api.tailscale.com/api/v2/device/${deviceId}/tags`, - { - "tags": [ - `tag:${userEmail}` - ] - }, { - headers: { - 'Authorization': `Bearer ${process.env.TAILSCALE_API_TOKEN}`, - 'Content-Type': 'application/json' - } - }); - break; - } catch (error) { - log.info(`Attempt ${attempts} Failed. Backing off for 30 seconds`) - } - } - - if (attempts === maxRetries) { - try { - throw new Error(`Failed to set tags in tailscale after ${attempts} retries`); - } catch (error) { - log.error({message: error.message, stack: error.stack}); - app.client.chat.postEphemeral({ - channel: `${body.channel.id}`, - user: `${body.user.id}`, - text: `Failed to set tags in tailscale` - }); - return; - } - } - - //get servers and info from tailscale - const { deviceIP } = await getDevices(serverName); - - //return info for tailscale - app.client.chat.postEphemeral({ - channel: `${body.channel.id}`, - user: `${body.user.id}`, - text: `The server has been created: https://login.tailscale.com/admin/machines/${deviceIP}` - }); - }, - - //delete a hetzner server - deleteServer: async ({ app, body, serverName }) => { - //get server from hetzner - const data = await getServer(); - - //get servers from tailscale - const { deviceId } = await getDevices(serverName) - - - //get server id from hetzner - let vmid = null - for (const server of data.data.servers) { - if (server.name === serverName) { - vmid = server.id; - } - } - - //delete the device from tailscale - await axios.delete(`https://api.tailscale.com/api/v2/device/${deviceId}`, { - headers: { - 'Authorization': `Bearer ${process.env.TAILSCALE_API_TOKEN}` - } - }) - .catch(error => { - log.error('Failed to delete device in tailscale', axiosError(error)); - - app.client.chat.postEphemeral({ - channel: `${body.channel.id}`, - user: `${body.user.id}`, - text: `Failed to delete Server: ${serverName} from Tailscale` - }); - }); - - //delete server from hetzner - try { - await axios.delete(`https://api.hetzner.cloud/v1/servers/${vmid}`, { - headers: { - 'Authorization': `Bearer ${process.env.HETZNER_API_TOKEN}` - } - }); - - app.client.chat.postEphemeral({ - channel: `${body.channel.id}`, - user: `${body.user.id}`, - text: `Server: ${serverName} has been deleted.` - }); - } catch (error) { - log.error('Failed to delete the server in hetzner', axiosError(error)); - - app.client.chat.postEphemeral({ - channel: `${body.channel.id}`, - user: `${body.user.id}`, - text: `Failed to delete Server: ${serverName} from Hetzner.` - }); - } - }, - - //list the servers - listServers: async ({ app, body }) => { - const data = await getServer(); - - // Call the users.info method using the WebClient - const info = await app.client.users.info({ - user: body.user.id - }) - .catch(error => { - log.error('There was an error getting user.info from slack', error); - }); - - const userEmail = formatUser(info.user.profile.email); - - //list the servers and build the buttons - for (const server of data.data.servers) { - if (server.labels.owner === userEmail) { - const { deviceIP } = await getDevices(server.name); - app.client.chat.postEphemeral({ - channel: `${body.channel.id}`, - user: `${body.user.id}`, - blocks: [ - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": `Server: ${server.name}\nServer id: ${server.id}\nStatus: ${server.status}\nConnect: https://login.tailscale.com/admin/machines/${deviceIP}` - } - }, - { - "type": "actions", - "elements": [ - { - "type": "button", - "text": { - "type": "plain_text", - "text": "Start" - }, - "action_id": `button_start_${server.id}` - }, - { - "type": "button", - "text": { - "type": "plain_text", - "text": "Stop" - }, - "action_id": `button_stop_${server.id}` - }, - { - "type": "button", - "text": { - "type": "plain_text", - "text": "Delete" - }, - "action_id": `button_delete_${server.name}` - } - ] - } - ], - text: "VM options" - }) - } - } - }, - - //start a hetzner server - startServer: async ({ app, body, vmid }) => { - try { - await axios.post(`https://api.hetzner.cloud/v1/servers/${vmid}/actions/poweron`, null, { - headers: { - 'Authorization': `Bearer ${process.env.HETZNER_API_TOKEN}` - } - }); - - app.client.chat.postEphemeral({ - channel: `${body.channel.id}`, - user: `${body.user.id}`, - text: `Server Id: ${vmid} has been started.` - }); - - } catch (error) { - log.error('Error starting the server', axiosError(error)); - app.client.chat.postEphemeral({ - channel: `${body.channel.id}`, - user: `${body.user.id}`, - text: `Server Id: ${vmid} failed to start.` - }); - - return; - } - }, - - //stop a hetzner server - stopServer: async ({ app, body, vmid }) => { - try { - await axios.post(`https://api.hetzner.cloud/v1/servers/${vmid}/actions/poweroff`, null, { - headers: { - 'Authorization': `Bearer ${process.env.HETZNER_API_TOKEN}` - } - }); - - app.client.chat.postEphemeral({ - channel: `${body.channel.id}`, - user: `${body.user.id}`, - text: `Server Id: ${vmid} has been stopped.` - }); - } catch (error) { - log.error('Error stopping the server', axiosError(error)); - app.client.chat.postEphemeral({ - channel: `${body.channel.id}`, - user: `${body.user.id}`, - text: `Server Id: ${vmid} failed to stop.` - }); - - return; - } - }, - - //code to build button UI - selectImage: async ({app, body }) => { - - //get the hetzner images - const images = await getHetznerImages(); - - //return if it fails to get the images. - if (!images) { - app.client.chat.postEphemeral({ - channel: `${body.channel.id}`, - user: `${body.user.id}`, - text: `Failed to get image data` - }); - - return; - } - - //build button for user to select - app.client.chat.postEphemeral({ - channel: `${body.channel.id}`, - user: `${body.user.id}`, - text: `Select an image:` - }); - for (const image of images) { - app.client.chat.postEphemeral({ - channel: `${body.channel.id}`, - user: `${body.user.id}`, - blocks: [ - { - "type": "actions", - "elements": [ - { - "type": "button", - "text": { - "type": "plain_text", - "text": `${image.description}` - }, - "action_id": `button_create_image_${image.description}_${image.id}` - }, - ] - } - ], - text: "Select an image:" - }) - } - } -} \ No newline at end of file