diff --git a/.env.example b/.env.example index fb8e65e..1c42dcb 100644 --- a/.env.example +++ b/.env.example @@ -6,9 +6,12 @@ PG_USER= PG_PASSWORD= PG_HOST= PG_DATABASE= +PG_PORT= REDIS_URL= CLOUDFLARE_API_TOKEN= CLOUDFLARE_ZONE_ID= CLOUDFLARE_GLOBAL_TOKEN= CLOUDFLARE_EMAIL= CERTBOT_EMAIL= +GITHUB_USERNAME= +GITHUB_TOKEN= diff --git a/.gitignore b/.gitignore index cb0f782..4b56acf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,56 @@ -node_modules/* -dist/* -package-lock.json -id_rsa -id_rsa.pub +# compiled output +/dist +/node_modules +/build + +# Logs +logs +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# OS +.DS_Store + +# Tests +/coverage +/.nyc_output + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# dotenv environment variable files .env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# temp directory +.temp +.tmp + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json diff --git a/README.md b/README.md index 6872b29..483d82a 100644 --- a/README.md +++ b/README.md @@ -119,4 +119,3 @@ This program is free software: you can redistribute it and/or modify it under th --- Made with ❤️ by Akshat Kotpalliwar (alias IntegerAlex on GitHub) -``` diff --git a/db/create.ts b/db/create.ts deleted file mode 100644 index 71e323f..0000000 --- a/db/create.ts +++ /dev/null @@ -1,31 +0,0 @@ -import database from './main'; - -export async function createDatabase() { - const dbName = process.env.PG_DATABASE; - const query = `CREATE DATABASE ${dbName};`; - - try { - await database.dbQuery(query); - console.log(`Database '${dbName}' created successfully.`); - } catch (error) { - console.error('Error creating database:', error); - } -} - -export async function createTableDeployments() { - try { - await database.dbQuery(` - CREATE TABLE IF NOT EXISTS deployments ( - project_name VARCHAR(255) NOT NULL, - id SERIAL PRIMARY KEY, - container_id VARCHAR(255) NOT NULL, - user_name VARCHAR(255) NOT NULL, - time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - status VARCHAR(255) NOT NULL - ); - `); - console.log("Table 'deployments' created or already exists."); - } catch (error) { - console.error('Error creating table:', error); - } -} diff --git a/db/main.ts b/db/main.ts deleted file mode 100644 index 1550082..0000000 --- a/db/main.ts +++ /dev/null @@ -1,94 +0,0 @@ -import pg from 'pg'; -import { createClient } from 'redis'; -import { createDatabase, createTableDeployments } from './create'; -require('dotenv').config(); - -class db { - private client: pg.Client; - private redisClient; - - constructor() { - // Initialize PostgreSQL client - this.client = new pg.Client({ - user: process.env.PG_USER, // Replace with your local username - host: process.env.PG_HOST, // Replace with your local host - database: process.env.PG_DATABASE, // Replace with your local database name - password: process.env.PG_PASSWORD, // Replace with your local password - port: 5432, // Default PostgreSQL port - }); - - // Connect to PostgreSQL - this.client - .connect() - .then(() => { - console.log('Connected to PostgreSQL database'); - }) - .catch((err) => { - console.error('Error connecting to PostgreSQL database:', err); - }); - - // Initialize Redis client - this.redisClient = createClient({ - url: process.env.REDIS_URL, - }); - - // Handle Redis connection errors - this.redisClient.on('error', (err) => { - console.error('Redis Client Error:', err); - }); - - // Connect to Redis - this.redisClient - .connect() - .then(() => { - console.log('Connected to Redis'); - }) - .catch((err) => { - console.error('Error connecting to Redis:', err); - }); - } - - // Method to execute a PostgreSQL query - async dbQuery(query: string, values?: any[]): Promise { - try { - // test remove before production - if (query == 'integeralex') { - return null; - } - const result = await this.client.query(query, values); - return result; - } catch (error) { - console.error('Database query error:', error); - throw error; - } - } - - async dbRedisSet(key: string, value: boolean): Promise { - try { - await this.redisClient.set(key, value.toString()); - console.log('Redis set reply: OK'); - } catch (error) { - console.error('Redis set error:', error); - throw error; - } - } - - // Method to get a boolean value from Redis - async dbRedisGet(key: string): Promise { - try { - const value = await this.redisClient.get(key); - if (value === null) { - console.log('Key not found'); - return null; - } - return value === 'true'; - } catch (error) { - console.error('Redis get error:', error); - throw error; - } - } -} - -const database = new db(); -export default database; -createTableDeployments(); diff --git a/db/operations.ts b/db/operations.ts deleted file mode 100644 index fb06758..0000000 --- a/db/operations.ts +++ /dev/null @@ -1,39 +0,0 @@ -import database from './main'; - -export async function getDeployments(userName: string) { - const query = 'SELECT * FROM deployments WHERE user_name = $1'; - const values = [userName]; - - try { - const result = await database.dbQuery(query, values); - return result.rows; - } catch (err) { - console.error('Error fetching deployments:', err); - throw err; - } -} -export async function postDeployment( - projectName: string, - userName: string, - containerId: string -) { - const query = ` - INSERT INTO deployments (project_name, user_name, container_id, status) - VALUES ($1, $2, $3, 'deployed'); - `; - - try { - // Use parameterized query to avoid SQL injection and syntax issues - await database.dbQuery(query, [projectName, userName, containerId]); - console.log('Deployment inserted successfully.'); - } catch (error) { - console.error('Error inserting deployment:', error); - throw error; // Re-throw the error to handle it in the calling function - } -} - -export async function deleteDeployment(userName: string, containerId: string) { - database.dbQuery( - `DELETE FROM deployments WHERE user_name = ${userName} AND container_id = ${containerId}` - ); -} diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..0cdee2e --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,35 @@ +// @ts-check +import eslint from '@eslint/js'; +import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { + ignores: ['eslint.config.mjs'], + }, + eslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + eslintPluginPrettierRecommended, + { + languageOptions: { + globals: { + ...globals.node, + ...globals.jest, + }, + ecmaVersion: 2020, + sourceType: 'module', + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + { + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-floating-promises': 'warn', + '@typescript-eslint/no-unsafe-argument': 'warn' + }, + }, +); diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/frontend/components/Dashboard.jsx b/frontend/components/Dashboard.jsx new file mode 100644 index 0000000..9be6558 --- /dev/null +++ b/frontend/components/Dashboard.jsx @@ -0,0 +1,41 @@ +import React, { useState } from "react"; +import DeploymentList from "./DeploymentList"; +import DeploymentForm from "./DeploymentForm"; + +const Dashboard = () => { + const [showDeployFormOnly, setShowDeployFormOnly] = useState(false); + + return ( +
+
+ +
+ +
+ {!showDeployFormOnly && ( + + )} +
+ +
+
+
+ ); +}; + +export default Dashboard; + diff --git a/frontend/components/DeploymentForm.jsx b/frontend/components/DeploymentForm.jsx new file mode 100644 index 0000000..cb3b800 --- /dev/null +++ b/frontend/components/DeploymentForm.jsx @@ -0,0 +1,82 @@ +import React, { useEffect, useState } from "react"; + +const DeploymentForm = () => { + const [repositories, setRepositories] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + // Fetch repositories when component mounts + useEffect(() => { + fetch("/htmx/v1/repositories") + .then((response) => response.json()) + .then((data) => { + setRepositories(data.data.repositories); + setIsLoading(false); + }) + .catch((error) => { + console.error("Error fetching repositories:", error); + setIsLoading(false); + }); + }, []); + + // Handle form submission + const handleFormSubmit = (event) => { + event.preventDefault(); + const formData = new FormData(event.target); + const data = Object.fromEntries(formData.entries()); + console.log("Form Data:", data); + + fetch("/htmx/v1/deploy", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }) + .then((response) => response.json()) + .then((data) => { + const responseContainer = document.getElementById("response-container"); + responseContainer.innerHTML = `

${data.message}

`; + }) + .catch((error) => { + console.error("Error:", error); + }); + }; + + return ( +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ ); +}; + +export default DeploymentForm; + diff --git a/frontend/components/DeploymentList.jsx b/frontend/components/DeploymentList.jsx new file mode 100644 index 0000000..08a962d --- /dev/null +++ b/frontend/components/DeploymentList.jsx @@ -0,0 +1,20 @@ +import React from "react"; + +const DeploymentList = () => { + return ( +
+ {/* Replace with dynamic fetch/render logic */} +
+

My App

+

Status: Running

+
+
+

Test Site

+

Status: Building

+
+
+ ); +}; + +export default DeploymentList; + diff --git a/frontend/components/LoginForm.jsx b/frontend/components/LoginForm.jsx new file mode 100644 index 0000000..d45dd74 --- /dev/null +++ b/frontend/components/LoginForm.jsx @@ -0,0 +1,51 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +function LoginForm() { + const [passKey, setPassKey] = useState(''); + const [errorMessage, setErrorMessage] = useState(''); + const navigate = useNavigate(); + + const handleSubmit = async (e) => { + e.preventDefault(); + setErrorMessage(''); + try { + const response = await fetch('/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ passKey }) + }); + + if (response.ok) { + const data = await response.json(); + sessionStorage.setItem('auth_token', data.token); + navigate('/dashboard'); + } else { + const errorData = await response.json(); + setErrorMessage(errorData.message || 'Invalid credentials'); + } + } catch (error) { + setErrorMessage('Connection error'); + } + }; + + return ( +
+

Login

+
+ setPassKey(e.target.value)} + placeholder="Enter Pass Key" + required + /> +
{errorMessage}
+ +
+
+ ); +} + +export default LoginForm; + diff --git a/frontend/dashboard.html b/frontend/dashboard.html deleted file mode 100644 index 3df3454..0000000 --- a/frontend/dashboard.html +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - Project Setup Dashboard - - - - - -
-
- -
-
- -
-
-
- - -
-
- -
- -
- - -
-
- - -
-
- - -
- -
-
-
Deploying...
-
-
-
- - - - - diff --git a/frontend/dashboard.png b/frontend/dashboard.png deleted file mode 100755 index e357094..0000000 Binary files a/frontend/dashboard.png and /dev/null differ diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..ec2b712 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,33 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' + +export default [ + { ignores: ['dist'] }, + { + files: ['**/*.{js,jsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...js.configs.recommended.rules, + ...reactHooks.configs.recommended.rules, + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +] diff --git a/frontend/home.css b/frontend/home.css deleted file mode 100644 index cf8f090..0000000 --- a/frontend/home.css +++ /dev/null @@ -1,303 +0,0 @@ -:root { - --background: #F0F4F8; - --foreground: #222831; - --primary: #00A8CC; - --primary-foreground: #FFFFFF; - --secondary: #FFFFFF; - --secondary-foreground: #00A8CC; - --accent: #FF6F61; - --accent-foreground: #FFFFFF; - --destructive: #FF5A5F; - --destructive-foreground: #FFFFFF; - --muted: #F0F4F8; - --muted-foreground: #222831; - --card: #FFFFFF; - --card-foreground: #222831; - --popover: #FFFFFF; - --popover-foreground: #222831; - --border: #E0E0E0; - --input: #D6E4E5; - --ring: #B2DFDB; - --radius: 0.75rem; - --shadow-sm: 0 4px 8px rgba(0, 0, 0, 0.1); - --shadow-md: 0 8px 16px rgba(0, 0, 0, 0.2); - - -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; - border-color: var(--border); -} - -body { - background-color: var(--background); - color: var(--foreground); - font-family: 'Poppins', sans-serif; - line-height: 1.6; -} - -h1, h2, h3, h4, h5, h6 { - font-weight: 700; -} - -a { - color: var(--primary); - text-decoration: none; - transition: color 0.3s; -} - -a:hover, a:focus-visible { - color: var(--accent); -} - -button { - border: none; - cursor: pointer; - font: inherit; - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: var(--radius); - transition: background-color 0.3s, color 0.3s, box-shadow 0.3s; -} - -.wrapper { - display: flex; - flex-direction: column; - min-height: 100vh; -} - -.site-header, .site-footer { - padding: 1rem 2rem; - background-color: var(--primary); - color: var(--primary-foreground); - box-shadow: var(--shadow-sm); -} - - -.site-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 1rem 2rem; - background-color: var(--primary); - color: var(--primary-foreground); - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - position: sticky; - top: 0; - z-index: 1000; -} - -.main-nav { - display: flex; - gap: 1rem; -} - -.nav-item { - padding: 0.5rem 1rem; - border-radius: var(--radius); - transition: background-color 0.3s, color 0.3s; - color: var(--primary-foreground); -} - -.nav-item:hover { - background-color: var(--accent); - color: var(--accent-foreground); -} - - - - - - - -.logo { - display: flex; - align-items: center; - font-size: 1.5rem; - font-weight: 700; -} - - - -.nav-item:hover { - background-color: var(--accent); - color: var(--accent-foreground); -} - -.content { - flex: 1; - background: linear-gradient(to bottom, var(--primary), var(--background)); - color: var(--primary-foreground); -} - -.hero-section { - padding: 4rem 2rem; - text-align: center; -} - -.hero-content { - display: flex; - flex-direction: column; - align-items: center; - max-width: 600px; - margin: 0 auto; -} - -.hero-title { - font-size: clamp(2rem, 5vw, 3rem); - margin-bottom: 1.5rem; - text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2); -} - -.hero-description { - font-size: clamp(1rem, 2.5vw, 1.25rem); - margin-bottom: 2rem; -} - -.hero-buttons { - display: flex; - gap: 1rem; -} - -.hero-image { - margin-top: 3rem; - max-width: 100%; - height: auto; - border-radius: var(--radius); - box-shadow: var(--shadow-md); -} - -.features-section, .benefits-section, .partners-section, .cta-section { - padding: 4rem 2rem; - text-align: center; -} - -.features-wrapper, .benefits-wrapper, .partners-wrapper, .cta-wrapper { - max-width: 1000px; - margin: 0 auto; -} - -.features-header { - margin-bottom: 2rem; -} - -.features-tag { - display: inline-block; - padding: 0.5rem 1rem; - background-color: var(--accent); - color: var(--accent-foreground); - border-radius: var(--radius); - font-size: 0.875rem; - font-weight: 700; - margin-bottom: 1rem; -} - -.features-list { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - gap: 2rem; - margin-top: 2rem; -} - -.features-item { - text-align: left; - background-color: var(--card); - padding: 2rem; - border-radius: var(--radius); - box-shadow: var(--shadow-sm); - transition: transform 0.3s; - will-change: transform; -} - -.features-item:hover { - transform: translateY(-10px); -} - -.features-item-title { - font-size: 1.5rem; - margin-bottom: 0.75rem; -} - -.features-item-description { - font-size: 1rem; - color: var(--foreground); -} - -.partners-logos { - display: flex; - flex-wrap: wrap; - gap: 2rem; - justify-content: center; - align-items: center; - margin-top: 2rem; -} - -.partner-logo { - max-height: 60px; - transition: transform 0.3s; - will-change: transform; -} - -.partner-logo:hover { - transform: scale(1.1); -} - -.cta-wrapper { - display: flex; - flex-direction: column; - align-items: center; -} - -.cta-image { - max-width: 100%; - height: auto; - margin-bottom: 2rem; - border-radius: var(--radius); - box-shadow: var(--shadow-md); -} - -.btn { - padding: 0.75rem 1.5rem; - font-size: 1rem; - font-weight: 700; -} - -.primary-btn { - background-color: var(--accent); - color: var(--accent-foreground); - box-shadow: var(--shadow-sm); -} - -.primary-btn:hover { - background-color: var(--accent-foreground); - color: var(--accent); -} - -.secondary-btn { - background-color: var(--secondary); - color: var(--secondary-foreground); -} - -.secondary-btn:hover { - background-color: var(--secondary-foreground); - color: var(--secondary); -} - -.footer-text { - text-align: center; - font-size: 0.875rem; -} - -@media (max-width: 768px) { - .hero-buttons { - flex-direction: column; - } - - .features-list { - grid-template-columns: 1fr; - } -} diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..40d78a6 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + + +
+ + + + diff --git a/frontend/index.js b/frontend/index.js deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/login.html b/frontend/login.html deleted file mode 100644 index 9f96607..0000000 --- a/frontend/login.html +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - Login - - - -

Login

-
- - -
- -
- - - \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..9a39e4e --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2815 @@ +{ + "name": "new", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "new", + "version": "0.0.0", + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router": "^7.4.1", + "react-router-dom": "^7.4.1" + }, + "devDependencies": { + "@eslint/js": "^9.21.0", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.21.0", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^15.15.0", + "vite": "^6.2.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", + "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", + "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz", + "integrity": "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.26.8", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz", + "integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz", + "integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", + "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.27.0", + "@babel/parser": "^7.27.0", + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", + "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", + "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", + "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", + "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", + "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", + "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", + "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", + "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", + "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", + "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", + "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", + "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", + "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", + "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", + "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", + "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", + "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", + "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", + "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", + "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", + "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", + "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", + "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", + "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", + "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz", + "integrity": "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", + "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", + "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.23.0.tgz", + "integrity": "sha512-35MJ8vCPU0ZMxo7zfev2pypqTwWTofFZO6m4KAtdoFhRpLJUpHTZZ+KB3C7Hb1d7bULYwO4lJXGCi5Se+8OMbw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.13.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.39.0.tgz", + "integrity": "sha512-lGVys55Qb00Wvh8DMAocp5kIcaNzEFTmGhfFd88LfaogYTRKrdxgtlO5H6S49v2Nd8R2C6wLOal0qv6/kCkOwA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.39.0.tgz", + "integrity": "sha512-It9+M1zE31KWfqh/0cJLrrsCPiF72PoJjIChLX+rEcujVRCb4NLQ5QzFkzIZW8Kn8FTbvGQBY5TkKBau3S8cCQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.39.0.tgz", + "integrity": "sha512-lXQnhpFDOKDXiGxsU9/l8UEGGM65comrQuZ+lDcGUx+9YQ9dKpF3rSEGepyeR5AHZ0b5RgiligsBhWZfSSQh8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.39.0.tgz", + "integrity": "sha512-mKXpNZLvtEbgu6WCkNij7CGycdw9cJi2k9v0noMb++Vab12GZjFgUXD69ilAbBh034Zwn95c2PNSz9xM7KYEAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.39.0.tgz", + "integrity": "sha512-jivRRlh2Lod/KvDZx2zUR+I4iBfHcu2V/BA2vasUtdtTN2Uk3jfcZczLa81ESHZHPHy4ih3T/W5rPFZ/hX7RtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.39.0.tgz", + "integrity": "sha512-8RXIWvYIRK9nO+bhVz8DwLBepcptw633gv/QT4015CpJ0Ht8punmoHU/DuEd3iw9Hr8UwUV+t+VNNuZIWYeY7Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.39.0.tgz", + "integrity": "sha512-mz5POx5Zu58f2xAG5RaRRhp3IZDK7zXGk5sdEDj4o96HeaXhlUwmLFzNlc4hCQi5sGdR12VDgEUqVSHer0lI9g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.39.0.tgz", + "integrity": "sha512-+YDwhM6gUAyakl0CD+bMFpdmwIoRDzZYaTWV3SDRBGkMU/VpIBYXXEvkEcTagw/7VVkL2vA29zU4UVy1mP0/Yw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.39.0.tgz", + "integrity": "sha512-EKf7iF7aK36eEChvlgxGnk7pdJfzfQbNvGV/+l98iiMwU23MwvmV0Ty3pJ0p5WQfm3JRHOytSIqD9LB7Bq7xdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.39.0.tgz", + "integrity": "sha512-vYanR6MtqC7Z2SNr8gzVnzUul09Wi1kZqJaek3KcIlI/wq5Xtq4ZPIZ0Mr/st/sv/NnaPwy/D4yXg5x0B3aUUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.39.0.tgz", + "integrity": "sha512-NMRUT40+h0FBa5fb+cpxtZoGAggRem16ocVKIv5gDB5uLDgBIwrIsXlGqYbLwW8YyO3WVTk1FkFDjMETYlDqiw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.39.0.tgz", + "integrity": "sha512-0pCNnmxgduJ3YRt+D+kJ6Ai/r+TaePu9ZLENl+ZDV/CdVczXl95CbIiwwswu4L+K7uOIGf6tMo2vm8uadRaICQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.39.0.tgz", + "integrity": "sha512-t7j5Zhr7S4bBtksT73bO6c3Qa2AV/HqiGlj9+KB3gNF5upcVkx+HLgxTm8DK4OkzsOYqbdqbLKwvGMhylJCPhQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.39.0.tgz", + "integrity": "sha512-m6cwI86IvQ7M93MQ2RF5SP8tUjD39Y7rjb1qjHgYh28uAPVU8+k/xYWvxRO3/tBN2pZkSMa5RjnPuUIbrwVxeA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.39.0.tgz", + "integrity": "sha512-iRDJd2ebMunnk2rsSBYlsptCyuINvxUfGwOUldjv5M4tpa93K8tFMeYGpNk2+Nxl+OBJnBzy2/JCscGeO507kA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.39.0.tgz", + "integrity": "sha512-t9jqYw27R6Lx0XKfEFe5vUeEJ5pF3SGIM6gTfONSMb7DuG6z6wfj2yjcoZxHg129veTqU7+wOhY6GX8wmf90dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.39.0.tgz", + "integrity": "sha512-ThFdkrFDP55AIsIZDKSBWEt/JcWlCzydbZHinZ0F/r1h83qbGeenCt/G/wG2O0reuENDD2tawfAj2s8VK7Bugg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.39.0.tgz", + "integrity": "sha512-jDrLm6yUtbOg2TYB3sBF3acUnAwsIksEYjLeHL+TJv9jg+TmTwdyjnDex27jqEMakNKf3RwwPahDIt7QXCSqRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.39.0.tgz", + "integrity": "sha512-6w9uMuza+LbLCVoNKL5FSLE7yvYkq9laSd09bwS0tMjkwXrmib/4KmoJcrKhLWHvw19mwU+33ndC69T7weNNjQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.39.0.tgz", + "integrity": "sha512-yAkUOkIKZlK5dl7u6dg897doBgLXmUHhIINM2c+sND3DZwnrdQkkSiDh7N75Ll4mM4dxSkYfXqU9fW3lLkMFug==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.0.tgz", + "integrity": "sha512-UaicktuQI+9UKyA4njtDOGBD/67t8YEBt2xdfqu8+gP9hqPUPsiXlNPcpS2gVdjmis5GKPG3fCxbQLVgxsQZ8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.1.tgz", + "integrity": "sha512-jFf/woGTVTjUJsl2O7hcopJ1r0upqoq/vIOoCj0yLh3RIXxWcljlpuZ+vEBRXsymD1jhfeJrlyTy/S1UW+4y1w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", + "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.26.0", + "@babel/plugin-transform-react-jsx-self": "^7.25.9", + "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" + } + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001710", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001710.tgz", + "integrity": "sha512-B5C0I0UmaGqHgo5FuqJ7hBd4L57A4dDD+Xi+XX1nXOoxGeDdY4Ko38qJYOyqznBVJEqON5p8P1x5zRR3+rsnxA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.132", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.132.tgz", + "integrity": "sha512-QgX9EBvWGmvSRa74zqfnG7+Eno0Ak0vftBll0Pt2/z5b3bEGYL6OUXLgKPtvx73dn3dvwrlyVkjPKRRlhLYTEg==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", + "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.2", + "@esbuild/android-arm": "0.25.2", + "@esbuild/android-arm64": "0.25.2", + "@esbuild/android-x64": "0.25.2", + "@esbuild/darwin-arm64": "0.25.2", + "@esbuild/darwin-x64": "0.25.2", + "@esbuild/freebsd-arm64": "0.25.2", + "@esbuild/freebsd-x64": "0.25.2", + "@esbuild/linux-arm": "0.25.2", + "@esbuild/linux-arm64": "0.25.2", + "@esbuild/linux-ia32": "0.25.2", + "@esbuild/linux-loong64": "0.25.2", + "@esbuild/linux-mips64el": "0.25.2", + "@esbuild/linux-ppc64": "0.25.2", + "@esbuild/linux-riscv64": "0.25.2", + "@esbuild/linux-s390x": "0.25.2", + "@esbuild/linux-x64": "0.25.2", + "@esbuild/netbsd-arm64": "0.25.2", + "@esbuild/netbsd-x64": "0.25.2", + "@esbuild/openbsd-arm64": "0.25.2", + "@esbuild/openbsd-x64": "0.25.2", + "@esbuild/sunos-x64": "0.25.2", + "@esbuild/win32-arm64": "0.25.2", + "@esbuild/win32-ia32": "0.25.2", + "@esbuild/win32-x64": "0.25.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.23.0.tgz", + "integrity": "sha512-jV7AbNoFPAY1EkFYpLq5bslU9NLNO8xnEeQXwErNibVryjk67wHVmddTBilc5srIttJDBrB0eMHKZBFbSIABCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.2", + "@eslint/config-helpers": "^0.2.0", + "@eslint/core": "^0.12.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.23.0", + "@eslint/plugin-kit": "^0.2.7", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.19.tgz", + "integrity": "sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.4.1.tgz", + "integrity": "sha512-Vmizn9ZNzxfh3cumddqv3kLOKvc7AskUT0dC1prTabhiEi0U4A33LmkDOJ79tXaeSqCqMBXBU/ySX88W85+EUg==", + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.4.1.tgz", + "integrity": "sha512-L3/4tig0Lvs6m6THK0HRV4eHUdpx0dlJasgCxXKnavwhh4tKYgpuZk75HRYNoRKDyDWi9QgzGXsQ1oQSBlWpAA==", + "license": "MIT", + "dependencies": { + "react-router": "7.4.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.39.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.39.0.tgz", + "integrity": "sha512-thI8kNc02yNvnmJp8dr3fNWJ9tCONDhp6TV35X6HkKGGs9E6q7YWCHbe5vKiTa7TAiNcFEmXKj3X/pG2b3ci0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.7" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.39.0", + "@rollup/rollup-android-arm64": "4.39.0", + "@rollup/rollup-darwin-arm64": "4.39.0", + "@rollup/rollup-darwin-x64": "4.39.0", + "@rollup/rollup-freebsd-arm64": "4.39.0", + "@rollup/rollup-freebsd-x64": "4.39.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.39.0", + "@rollup/rollup-linux-arm-musleabihf": "4.39.0", + "@rollup/rollup-linux-arm64-gnu": "4.39.0", + "@rollup/rollup-linux-arm64-musl": "4.39.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.39.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.39.0", + "@rollup/rollup-linux-riscv64-gnu": "4.39.0", + "@rollup/rollup-linux-riscv64-musl": "4.39.0", + "@rollup/rollup-linux-s390x-gnu": "4.39.0", + "@rollup/rollup-linux-x64-gnu": "4.39.0", + "@rollup/rollup-linux-x64-musl": "4.39.0", + "@rollup/rollup-win32-arm64-msvc": "4.39.0", + "@rollup/rollup-win32-ia32-msvc": "4.39.0", + "@rollup/rollup-win32-x64-msvc": "4.39.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==", + "license": "ISC" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.5.tgz", + "integrity": "sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "postcss": "^8.5.3", + "rollup": "^4.30.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..cde92b6 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,28 @@ +{ + "name": "new", + "private": true, + "version": "0.0.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-router": "^7.4.1", + "react-router-dom": "^7.4.1" + }, + "devDependencies": { + "@eslint/js": "^9.21.0", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.21.0", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^15.15.0", + "vite": "^6.2.0" + } +} diff --git a/frontend/serve.jpg b/frontend/serve.jpg deleted file mode 100755 index b58510d..0000000 Binary files a/frontend/serve.jpg and /dev/null differ diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..3f118c4 --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; +import LoginForm from '../components/LoginForm'; +import Dashboard from '../components/Dashboard'; + +function App() { + return ( + + + } /> + } /> + + + ); +} + +export default App; + diff --git a/frontend/src/Navbar.jsx b/frontend/src/Navbar.jsx new file mode 100644 index 0000000..dd73d25 --- /dev/null +++ b/frontend/src/Navbar.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +function Navbar() { + return ( + + ); +} + +export default Navbar; + diff --git a/frontend/src/index.jsx b/frontend/src/index.jsx new file mode 100644 index 0000000..9baadbc --- /dev/null +++ b/frontend/src/index.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import '../style/base.css'; // Optionally import your CSS here (or base.css) + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); + diff --git a/frontend/style/base.css b/frontend/style/base.css new file mode 100644 index 0000000..a7cece4 --- /dev/null +++ b/frontend/style/base.css @@ -0,0 +1,148 @@ +/* RESET & BASE STYLES */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background-color: #f5f7fa; + color: #333; + line-height: 1.6; + padding: 0; + margin: 0; +} + +a { + color: inherit; + text-decoration: none; +} + +/* BUTTONS */ +button, .btn { + background-color: #0057ff; + color: white; + border: none; + padding: 0.6rem 1.2rem; + border-radius: 6px; + cursor: pointer; + font-size: 1rem; + transition: background 0.3s ease; +} + +button:hover, .btn:hover { + background-color: #003fcc; +} + +.primary-btn { + background-color: #007bff; +} + +.primary-btn:hover { + background-color: #0056b3; +} + +/* FORM ELEMENTS */ +input, select, textarea { + width: 100%; + padding: 0.5rem; + margin-top: 0.25rem; + margin-bottom: 1rem; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 1rem; +} + +label { + display: block; + font-weight: 500; + margin-bottom: 0.25rem; +} + +.form-group { + margin-bottom: 1rem; +} + +/* ERROR & FEEDBACK */ +.error { + color: red; + margin-top: 0.5rem; +} + +.loading { + margin-top: 1rem; + color: #888; +} + +/* LAYOUT */ +.dashboard-main { + padding: 2rem; + display: flex; + flex-direction: column; +} + +.dashboard-header { + display: flex; + justify-content: flex-start; + margin-bottom: 1rem; +} + +.dashboard-body { + display: flex; + gap: 1.5rem; +} + +.deployments-container { + background: white; + padding: 1rem; + border-radius: 10px; + box-shadow: 0 2px 6px rgba(0,0,0,0.05); + flex: 1; + max-width: 300px; +} + +.project-setup-container { + background: white; + padding: 1.5rem; + border-radius: 10px; + box-shadow: 0 2px 6px rgba(0,0,0,0.05); + flex: 2; +} + +/* DEPLOYMENT ITEMS */ +.deployment-item { + background-color: #f0f2f5; + padding: 1rem; + margin-bottom: 1rem; + border-radius: 8px; + cursor: pointer; + transition: background 0.2s; +} + +.deployment-item:hover { + background-color: #e1e4e8; +} + +/* LOGIN PAGE */ +.login-container { + max-width: 400px; + margin: 4rem auto; + background: white; + padding: 2rem; + border-radius: 12px; + box-shadow: 0 5px 20px rgba(0, 0, 0, 0.05); +} + +/* RESPONSIVENESS */ +@media (max-width: 768px) { + .dashboard-body { + flex-direction: column; + } + + .deployments-container, + .project-setup-container { + max-width: 100%; + } +} + diff --git a/frontend/styles.css b/frontend/styles.css deleted file mode 100644 index 89c99ed..0000000 --- a/frontend/styles.css +++ /dev/null @@ -1,175 +0,0 @@ -/* Root color variables */ -:root { - --background: hsl(210, 100%, 97%); - --foreground: hsl(339, 20%, 20%); - --primary: hsl(308, 56%, 85%); - --primary-foreground: hsl(210, 22%, 22%); - --secondary: hsl(196, 75%, 88%); - --secondary-foreground: hsl(210, 22%, 22%); - --accent: hsl(211, 86%, 70%); - --accent-foreground: hsl(210, 22%, 22%); - --destructive: hsl(0, 93%, 73%); - --destructive-foreground: hsl(210, 22%, 22%); - --muted: hsl(210, 100%, 95%); - --muted-foreground: hsl(210, 22%, 22%); - --card: hsl(210, 100%, 97%); - --card-foreground: hsl(210, 22%, 22%); - --popover: hsl(0, 0%, 100%); - --popover-foreground: hsl(341, 20%, 22%); - --border: hsl(210, 40%, 80%); - --input: hsl(210, 40%, 56%); - --ring: hsl(210, 40%, 60%); - --radius: 1rem; -} - -/* General reset and base styles */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; - border-color: var(--border); -} - -body { - background-color: var(--background); - color: var(--foreground); - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; - line-height: 1.5; -} - -h1, h2, h3, h4, h5, h6 { - font-weight: 700; -} - -a { - color: var(--primary); - text-decoration: none; -} - -a:hover, a:focus { - text-decoration: underline; -} - -button { - border: none; - cursor: pointer; - font: inherit; - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: var(--radius); -} - -.dashboard { - display: flex; - flex-direction: column; - min-height: 100vh; -} - -.dashboard-header, .dashboard-footer { - padding: 1rem 2rem; - background-color: var(--card); -} - -/* Navbar styles */ -.navbar { - display: flex; - align-items: center; - justify-content: space-between; - padding: 1rem; - background-color: var(--card); -} - -.navbar-left { - display: flex; - align-items: center; -} - -.navbar-left img.profile-image { - margin-right: 1rem; /* Adds space between the profile image and any adjacent elements */ -} - -.navbar-right { - display: flex; - align-items: center; - margin-left: auto; /* Pushes the navbar-right section to the far right */ -} - -.profile-image { - border-radius: 50%; - width: 40px; - height: 40px; -} - -.dashboard-main { - display: flex; - flex: 1; - padding: 2rem; -} - -.deployments-container { - flex: 1; - margin-right: 2rem; - background-color: var(--card); - padding: 1rem; - border-radius: var(--radius); -} - -.project-setup-container { - flex: 2; - background-color: var(--card); - padding: 1rem; - border-radius: var(--radius); -} - -.form-group { - margin-bottom: 1rem; -} - -.form-group label { - display: block; - margin-bottom: 0.5rem; - font-weight: 700; -} - -.form-group select, -.form-group input { - width: 100%; - padding: 0.5rem; - border: 1px solid var(--border); - border-radius: var(--radius); - font-size: 1rem; -} - -.btn { - padding: 0.75rem 1.5rem; - font-size: 1rem; - font-weight: 700; - transition: background-color 0.3s; -} - -.primary-btn { - background-color: var(--accent); - color: var(--accent-foreground); -} - -.primary-btn:hover { - background-color: var(--accent-foreground); - color: var(--accent); -} - -.secondary-btn { - background-color: var(--secondary); - color: var(--secondary-foreground); -} - -.secondary-btn:hover { - background-color: var(--secondary-foreground); - color: var(--secondary); -} - -.footer-text { - text-align: center; - font-size: 0.875rem; -} - diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..8b0f57b --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/nest-cli.json b/nest-cli.json new file mode 100644 index 0000000..f9aa683 --- /dev/null +++ b/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/package.json b/package.json index c1630c8..61966db 100644 --- a/package.json +++ b/package.json @@ -1,52 +1 @@ -{ - "name": "flexr", - "version": "1.0.0", - "main": "dist/server/setup.js", - "scripts": { - "dev": "nodemon --exec ts-node server/setup.ts", - "build": "tsc", - "start": "node dist/server/setup.js", - "lint": "eslint . --ext .ts", - "test": "jest", - "db:migrate": "node dist/db/create.js" - }, - "keywords": [ - "deployment", - "containers", - "podman", - "nodejs" - ], - "author": "Akshat Kotpalliwar", - "license": "GPL-3.0", - "description": "", - "dependencies": { - "cloudflare": "^3.5.0", - "compression": "^1.7.4", - "cookie-parser": "^1.4.7", - "cors": "^2.8.5", - "dotenv": "^16.4.5", - "express": "^4.19.2", - "helmet": "^7.1.0", - "htmx": "^0.0.2", - "pg": "^8.12.0", - "redis": "^4.7.0", - "uuid": "^11.1.0", - "winston": "^3.11.0" - }, - "devDependencies": { - "@types/cookie-parser": "^1.4.8", - "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", - "@types/jest": "^29.5.0", - "@types/node": "^20.14.12", - "@types/pg": "^8.11.6", - "@types/uuid": "^10.0.0", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", - "eslint": "^8.57.0", - "jest": "^29.7.0", - "nodemon": "^3.0.0", - "ts-jest": "^29.1.0", - "ts-node": "^10.9.2" - } -} +{"name":"fenrir","version":"0.0.1","description":"","author":"","private":true,"license":"UNLICENSED","scripts":{"build":"nest build","format":"prettier --write \"src/**/*.ts\" \"test/**/*.ts\"","start":"nest start","start:dev":"nest start --watch","start:debug":"nest start --debug --watch","start:prod":"node dist/main","lint":"eslint \"{src,apps,libs,test}/**/*.ts\" --fix","test":"jest","test:watch":"jest --watch","test:cov":"jest --coverage","test:debug":"node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand","test:e2e":"jest --config ./test/jest-e2e.json"},"dependencies":{"@nestjs/common":"^11.0.1","@nestjs/config":"^4.0.2","@nestjs/core":"^11.0.1","@nestjs/platform-express":"^11.0.1","@nestjs/serve-static":"^5.0.3","@nestjs/typeorm":"^11.0.0","ioredis":"^5.6.0","pg":"^8.14.1","reflect-metadata":"^0.2.2","rxjs":"^7.8.1","typeorm":"^0.3.21"},"devDependencies":{"@eslint/eslintrc":"^3.2.0","@eslint/js":"^9.23.0","@nestjs/cli":"^11.0.0","@nestjs/schematics":"^11.0.0","@nestjs/testing":"^11.0.1","@swc/cli":"^0.6.0","@swc/core":"^1.10.7","@types/express":"^5.0.0","@types/jest":"^29.5.14","@types/node":"^22.14.0","@types/supertest":"^6.0.2","eslint":"^9.23.0","eslint-config-prettier":"^10.0.1","eslint-plugin-prettier":"^5.2.2","globals":"^16.0.0","jest":"^29.7.0","prettier":"^3.4.2","source-map-support":"^0.5.21","supertest":"^7.0.0","ts-jest":"^29.2.5","ts-loader":"^9.5.2","ts-node":"^10.9.2","tsconfig-paths":"^4.2.0","typescript":"^5.7.3","typescript-eslint":"^8.29.0"},"jest":{"moduleFileExtensions":["js","json","ts"],"rootDir":"src","testRegex":".*\\.spec\\.ts$","transform":{"^.+\\.(t|j)s$":"ts-jest"},"collectCoverageFrom":["**/*.(t|j)s"],"coverageDirectory":"../coverage","testEnvironment":"node"}} \ No newline at end of file diff --git a/server/config.ts b/server/config.ts deleted file mode 100644 index f53936f..0000000 --- a/server/config.ts +++ /dev/null @@ -1,41 +0,0 @@ -interface Config { - server: { - port: number | string; - baseUrl: string; - }; - cloudflare: { - zoneId?: string; - email?: string; - token?: string; - }; - certbot: { - email?: string; - }; - redis: { - url: string; - }; - postgres: { - url: string; - }; -} - -export const config: Config = { - server: { - port: process.env.PORT || 8080, - baseUrl: process.env.BASEURL || 'http://localhost:8080', - }, - cloudflare: { - zoneId: process.env.CLOUDFLARE_ZONE_ID, - email: process.env.CLOUDFLARE_EMAIL, - token: process.env.CLOUDFLARE_GLOBAL_TOKEN, - }, - certbot: { - email: process.env.CERTBOT_EMAIL, - }, - redis: { - url: process.env.REDIS_URL || 'redis://localhost:6379', - }, - postgres: { - url: process.env.DATABASE_URL || 'postgresql://localhost:5432/flexhost', - }, -}; diff --git a/server/containerServices.ts b/server/containerServices.ts deleted file mode 100644 index f646012..0000000 --- a/server/containerServices.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { exec, execSync } from 'child_process'; -import { promisify } from 'util'; -import { createWriteStream, writeFileSync } from 'fs'; -import { getPort, dockerFile } from './utils/containerUtil'; -import { setupSubdomain } from './utils/serverUtils'; -import { postDeployment } from '../db/operations'; -const execAsync = promisify(exec); -const HOST = process.env.HOST || 'http://localhost:8080'; -const linuxUser = process.env.LINUX_USER || 'root'; -export async function runContainer( - username: string, - projectName: string -): Promise { - if (!username || !projectName) { - throw new Error('Invalid input'); - } - - try { - const port = await getPort(8081); - const imageName = `${username.toLowerCase()}-${projectName}`; - const { stdout } = await execAsync( - `podman run -d -p ${port}:8080 -t localhost/${imageName}:latest` - ); - createWriteStream('containerId.txt').write(stdout); - await postDeployment(projectName, username.toLowerCase(), stdout.trim()); - const link = `${HOST}/${stdout.trim().substring(0, 12)}`; - await setupSubdomain(stdout.trim().substring(0, 12), port, stdout.trim()); - return link; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`Error running container: ${error.message}`); - } else { - console.error('Error running container: Unknown error'); - } - throw error; - } -} - -export async function createImage( - username: string, - projectName: string, - repoLink: string, - entryPoint: string, - buildCommand: string, - runCommand: string -): Promise { - if (!username || !projectName || !repoLink) { - throw new Error('Invalid input'); - } - - try { - await generateDockerFile( - username, - projectName, - repoLink, - entryPoint, - buildCommand, - runCommand - ); - const imageName = `${username.toLowerCase()}-${projectName}`; - const { stdout } = await execAsync(`buildah build -t ${imageName} .`); - console.log(`Image built: ${stdout}`); - return imageName; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`Error creating image: ${error.message}`); - } else { - console.error('Error creating image: Unknown error'); - } - throw error; - } -} - -async function generateDockerFile( - username: string, - projectName: string, - repoLink: string, - entryPoint: string, - buildCommand: string, - runCommand: string -): Promise { - try { - // Change directory to username folder - process.chdir(`/home/${linuxUser}/${username.toLowerCase()}`); - - // Clone the repository into the project directory - await execAsync(`git clone ${repoLink}`); - process.chdir(projectName); // Change directory to the project folder - - // Check if Dockerfile already exists - try { - execSync('ls Dockerfile'); - console.log('Dockerfile already exists, skipping creation.'); - } catch { - // Create Dockerfile if it doesn't exist - writeFileSync( - 'Dockerfile', - dockerFile(entryPoint, buildCommand, runCommand) - ); - console.log('Dockerfile created.'); - } - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`Error generating Dockerfile: ${error.message}`); - } else { - console.error('Error generating Dockerfile: Unknown error'); - } - throw error; - } -} diff --git a/server/routes/htmx.ts b/server/routes/htmx.ts deleted file mode 100644 index b9d0386..0000000 --- a/server/routes/htmx.ts +++ /dev/null @@ -1,151 +0,0 @@ -import express, { Request, Response } from 'express'; -import path from 'path'; -import { getDeployments } from '../../db/operations'; -import database from '../../db/main'; - -const router = express.Router(); - -// Define the request body type for the POST route -interface RunContainerRequestBody { - userName: string; - buildCommand?: string; - runCommand?: string; - repoLink: string; - entryPoint?: string; -} - -// Define the response type for the runContainer API call -interface RunContainerResponse { - containerId: string; -} - -// Define the deployment type -interface Deployment { - id: string; - project_name: string; - status: string; - time: string; - container_id: string; -} - -// Define the error response type -interface ErrorResponse { - message?: string; -} - -router.post('/', async (req: Request, res: Response) => { - const { - userName, - buildCommand, - runCommand, - repoLink, - entryPoint, - }: RunContainerRequestBody = req.body; - const projectName = repoLink.split('/').pop()?.split('.')[0] || ''; - - try { - const redisData = await database.dbRedisGet(userName.toLowerCase()); - if (redisData) { - // Uncomment if you want to limit deployments - // return res.send(`

Deployments limit reached

`); - } - - console.log(projectName, repoLink, entryPoint); - const response = await fetch('http://localhost:8080/v1/runContainer', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - userName, - projectName, - repoLink, - buildCommand: buildCommand || 'npm install', - runCommand: runCommand || 'node', - entryPoint, - }), - }); - - // Check if the response is OK and parse the JSON - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); // Handle potential JSON parsing errors - console.error('Error response:', errorData); - return res.status(response.status).json({ - message: (errorData as ErrorResponse).message || 'Error occurred', - }); - } - - const runContainerData: RunContainerResponse = - (await response.json()) as RunContainerResponse; // Assert the type of runContainerData - const containerId = runContainerData.containerId; // Now TypeScript knows this is safe - res.send(`

Container ID: ${containerId}

`); - } catch (error) { - console.error('Error:', error); - res.send('Error'); - } -}); - -//db.addContainer(containerId , projectName , repoLink , entryPoint) - -//res.send("

Deploying... please Wait

"); - -router.get('/deployments', async (req: Request, res: Response) => { - try { - const userName = req.query.userName as string; - console.log(userName.toLowerCase()); - //const data = await response.json(); - // - // - // - // const userName = data.nickname; - - const deployments: Deployment[] = await getDeployments( - userName.toLowerCase() - ); - if (!deployments || deployments.length === 0) { - return res.send('

No deployments found

'); - } - - const deploymentsHTML = deployments - .map((deployment) => { - return ` -
-

${deployment.project_name}

-

Status: ${deployment.status}

-

Deployed at: ${new Date(deployment.time).toLocaleString()}

-

Deployment ID: ${deployment.container_id}

-
- `; - }) - .join(''); - - res.send(` -
- ${deploymentsHTML} -
- `); - } catch (error) { - console.error('Error fetching deployments:', error); - res.send('

Error fetching deployments

'); - } -}); - -router.get('/subscription', async (req: Request, res: Response) => { - console.log('subscription'); - const userName = req.query.userName as string; - try { - const data = await database.dbRedisGet(userName.toLowerCase()); - if (data) { - res.send(`

You are in free tier

-

Upgrade to premium to get more deployments

`); - } else { - res.send(`

You are in free tier

-

You can deploy one application

`); - } - } catch (error) { - console.error('Error fetching subscription:', error); - res.send('

Error fetching subscription

'); - } -}); - -export default router; diff --git a/server/setup.ts b/server/setup.ts deleted file mode 100644 index 75edb00..0000000 --- a/server/setup.ts +++ /dev/null @@ -1,251 +0,0 @@ -import express from 'express'; -import cookieParser from 'cookie-parser'; // Added cookie-parser import -import { runContainer, createImage } from './containerServices'; -import htmxRouter from './routes/htmx'; -import path from 'path'; -import bodyParser from 'body-parser'; -import cors from 'cors'; -require('dotenv').config(); -import { createDirectory } from './utils/containerUtil'; -import database from '../db/main'; -import { exec } from 'child_process'; - -const app = express(); - -// Add cookie-parser middleware before any routes that use cookies -app.use(cookieParser()); -app.set('trust proxy', true); -app.use(cors()); -app.use(express.json()); -app.use(bodyParser.json()); -app.use(bodyParser.urlencoded({ extended: true })); - -app.use('/htmx', htmxRouter); -app.use(express.static(path.join(__dirname, '../../frontend/'))); - -// Health Check -app.get('/v1/health', (req, res) => { - res.status(200).send('Healthy'); -}); - -// Endpoint to run container -app.post('/v1/runContainer', async (req, res) => { - const { - userName, - projectName, - repoLink, - entryPoint, - buildCommand = 'npm install', - runCommand = 'node', - } = req.body as { - userName: string; - projectName: string; - repoLink: string; - entryPoint: string; - buildCommand?: string; - runCommand?: string; - }; - - const missingFields = []; - if (!userName) missingFields.push('userName'); - if (!projectName) missingFields.push('projectName'); - if (!repoLink) missingFields.push('repoLink'); - if (!entryPoint) missingFields.push('entryPoint'); - - if (missingFields.length > 0) { - return res.status(400).json({ - error: 'Missing required fields', - fields: missingFields, - }); - } - - try { - const imageName = await createImage( - userName, - projectName, - repoLink, - entryPoint, - buildCommand, - runCommand - ); - const containerId = await runContainer(userName, projectName); - - await database.dbRedisSet(userName.toLowerCase(), true); - res.json({ - containerId, - imageName, - status: 'deployed', - }); - } catch (err: unknown) { - if (err instanceof Error) { - console.error(`Deployment error: ${err.message}`); - res.status(500).json({ - error: 'Deployment failed', - message: err.message, - }); - } else { - console.error('Deployment error: Unknown error'); - res.status(500).json({ - error: 'Deployment failed', - message: 'Unknown error occurred', - }); - } - } -}); - -// In-memory session storage -const sessions: { [key: string]: { username: string; createdAt: number } } = {}; - -// Simple session ID generator -function generateSessionId(): string { - return Math.random().toString(36).substring(2); -} - -const SESSION_EXPIRY_MS = 30 * 60 * 1000; // 30 minutes expiry - -// Login route that sets a cookie with the session ID -app.post('/login', (req, res) => { - const { githubUsername, passKey } = req.body; - - if (passKey === process.env.PASS_KEY) { - createDirectory(githubUsername); - const sessionId = generateSessionId(); - sessions[sessionId] = { username: githubUsername, createdAt: Date.now() }; - res.cookie('sessionId', sessionId, { httpOnly: true, secure: false }); - console.log('Session ID set:', sessionId); - res.status(200).json({ message: 'Login successful', githubUsername }); - } else { - res.status(401).json({ message: 'Unauthorized' }); - } -}); - -// Middleware to check authentication -const isAuthenticated = ( - req: express.Request, - res: express.Response, - next: express.NextFunction -) => { - const sessionId = req.cookies?.sessionId; - console.log('Session ID:', sessionId); - - if (!sessionId) { - console.log('No session ID found'); - return res.status(401).json({ message: 'Unauthorized' }); - } - - const session = sessions[sessionId]; - if (!session) { - console.log('Session not found for ID:', sessionId); - return res.status(401).json({ message: 'Unauthorized' }); - } - - const sessionAge = Date.now() - session.createdAt; - - if (sessionAge > SESSION_EXPIRY_MS) { - console.log('Session expired:', sessionId); - delete sessions[sessionId]; - return res - .status(401) - .json({ message: 'Session expired. Please log in again.' }); - } else { - (req as any).user = { username: session.username }; - console.log('User authenticated:', session.username); - next(); - } -}; - -// Protected route example -app.get('/protected-route', isAuthenticated, (req, res) => { - res.json({ message: 'This is a protected route' }); -}); - -// /home route serving home.html -app.get('/home', (req, res) => { - console.log('User accessed /home'); - res.sendFile(path.join(__dirname, '../../frontend/home.html')); -}); - -// Callback route -app.get('/callback', (req, res) => { - console.log(req.oidc?.user); - res.sendFile(path.join(__dirname, '../../frontend/index.html')); -}); - -// Extend Request interface for oidc (if used) -declare global { - namespace Express { - interface Request { - oidc?: { - user: { - nickname: string; - }; - }; - } - } -} - -// Profile route with authentication -app.get( - '/v1/profile', - isAuthenticated, - (req: express.Request, res: express.Response) => { - console.log('User accessed /profile'); - res.json({ nickname: (req as any).user.username }); - } -); - -// Route to fetch GitHub repositories -app.get('/v1/repositories', (req: express.Request, res: express.Response) => { - const user_id = req.query.user_id as string; - - fetch(`https://api.github.com/users/${user_id}/repos`, { method: 'GET' }) - .then((response) => response.json()) - .then((data: any) => { - const repositories = data.map( - (repo: { name: string; html_url: string }) => ({ - name: repo.name, - url: repo.html_url, - }) - ); - res.send({ - repositories: repositories, - avatar_url: data[0]?.owner?.avatar_url, - }); - }) - .catch((error) => { - console.error('Error fetching repositories:', error); - res.status(500).send('Error fetching repositories'); - }); -}); - -// Error handling middleware -app.use( - ( - err: Error, - req: express.Request, - res: express.Response, - next: express.NextFunction - ) => { - console.error(err.stack); - res.status(500).send('Something broke!'); - } -); - -// Request logging middleware -app.use( - (req: express.Request, res: express.Response, next: express.NextFunction) => { - console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`); - next(); - } -); - -// Route to serve the login page -app.get('/', (req, res) => { - const filePath = path.join(__dirname, '../../frontend/login.html'); - console.log('Login file path:', filePath); - res.sendFile(filePath); -}); - -app.listen(8080, () => { - console.log('Server is running on port 8080'); -}); diff --git a/server/utils/containerUtil.ts b/server/utils/containerUtil.ts deleted file mode 100644 index 09ddc4d..0000000 --- a/server/utils/containerUtil.ts +++ /dev/null @@ -1,57 +0,0 @@ -import net from 'net'; -import fs from 'fs'; - -export const dockerFile = ( - entryPoint: string, - buildCommand: string, - runCommand: string -): string => ` -FROM node:22-alpine -WORKDIR /app -COPY . . -RUN npm install && ${buildCommand} -EXPOSE 8080 -CMD ["${runCommand}", "${entryPoint}"] -`; - -function isPortInUse(port: number, host = '127.0.0.1'): Promise { - return new Promise((resolve) => { - const server = net.createServer(); - - server.once('error', (err) => { - if (err.name === 'EADDRINUSE') { - resolve(true); // Port is in use - } else { - resolve(false); // Other errors - } - }); - - server.once('listening', () => { - server.close(() => { - resolve(false); // Port is not in use - }); - }); - - server.listen(port, host); - }); -} - -export async function getPort(findPort: number): Promise { - // Check if the starting port is in use - const inUse = await isPortInUse(findPort); - if (inUse) { - // If the port is in use, try the next port - return getPort(findPort + 1); - } else { - // Port is available - return findPort; - } -} - -export function createDirectory(userName: string): boolean { - const linuxUser = process.env.LINUX_USER || 'root'; - if (!fs.existsSync(`/home/${linuxUser}/${userName.toLowerCase()}`)) { - fs.mkdirSync(`/home/${linuxUser}/${userName.toLowerCase()}`); - } - return true; -} diff --git a/server/utils/serverUtils.ts b/server/utils/serverUtils.ts deleted file mode 100755 index d170af0..0000000 --- a/server/utils/serverUtils.ts +++ /dev/null @@ -1,183 +0,0 @@ -import * as fs from 'fs'; -import { exec } from 'child_process'; -import fetch from 'node-fetch'; - -// Function to add DNS record -async function addRecord( - subdomain: string, - dnsRecordId: string -): Promise { - const zoneId = process.env.CLOUDFLARE_ZONE_ID; - const url = `https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records`; - const data = { - type: 'A', - name: `${subdomain}.flexr`, - content: '35.223.20.186', - ttl: 120, - proxied: false, - comment: 'Domain verification record', - tags: [], - id: dnsRecordId, - }; - - return fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Auth-Email': process.env.CLOUDFLARE_EMAIL || '', - 'X-Auth-Key': process.env.CLOUDFLARE_GLOBAL_TOKEN || '', - }, - body: JSON.stringify(data), - }) - .then((response) => { - if (response.ok) { - return 'DNS record updated'; - } else { - throw new Error('Error updating DNS record'); - } - }) - .catch((error) => { - throw new Error('Error adding DNS record: ' + error.message); - }); -} - -// Function to get SSL certificate using Certbot -function getSSL(subdomain: string): Promise { - return new Promise((resolve, reject) => { - exec( - `sudo certbot --nginx -d ${subdomain}.flexr.flexhost.tech --non-interactive --agree-tos --email ${process.env.CERTBOT_EMAIL} && ` + - `sudo systemctl reload nginx`, - (error, stdout, stderr) => { - if (error) { - console.error(`Certbot error: ${error}`); - reject('Error obtaining SSL certificate'); - return; - } - resolve('SSL certificate obtained'); - console.log(`Certbot stdout: ${stdout}`); - console.error(`Certbot stderr: ${stderr}`); - } - ); - }); -} - -// Function to create NGINX server block configuration -function NginxServerBlock(subdomain: string, port: number): Promise { - const serverBlockConfig = ` -server { - listen 443 ssl; - server_name ${subdomain}.flexr.flexhost.tech; - - ssl_certificate /etc/letsencrypt/live/${subdomain}.flexr.flexhost.tech/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/${subdomain}.flexr.flexhost.tech/privkey.pem; - - location / { - proxy_pass http://localhost:${port}; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } -} - `; - - return new Promise((resolve, reject) => { - fs.writeFile( - `/etc/nginx/sites-available/${subdomain}`, - serverBlockConfig, - { encoding: 'utf8' }, - (error) => { - if (error) { - console.error(`File write error: ${error}`); - reject('Error creating NGINX server block configuration'); - } else { - resolve('NGINX server block configuration created'); - } - } - ); - }); -} - -// Function to create symbolic link for NGINX server block -function NginxServerBlockSymLink(subdomain: string): Promise { - return new Promise((resolve, reject) => { - exec( - `sudo ln -s /etc/nginx/sites-available/${subdomain} /etc/nginx/sites-enabled/`, - (error, stdout, stderr) => { - if (error) { - console.error(`Symlink error: ${error}`); - reject('Error creating symbolic link'); - } else { - resolve('Symbolic link created'); - console.log(`Symlink stdout: ${stdout}`); - console.error(`Symlink stderr: ${stderr}`); - } - } - ); - }); -} - -// Function to restart NGINX -function restartNginx(): Promise { - return new Promise((resolve, reject) => { - exec('sudo systemctl reload nginx', (error, stdout, stderr) => { - if (error) { - console.error(`NGINX reload error: ${error}`); - reject('Error restarting NGINX'); - } else { - resolve('NGINX restarted'); - console.log(`NGINX reload stdout: ${stdout}`); - console.error(`NGINX reload stderr: ${stderr}`); - } - }); - }); -} - -// Combine functions to setup DNS, SSL, and NGINX server block -export async function setupSubdomain( - subdomain: string, - port: number, - dnsRecordID: string -): Promise { - return addRecord(subdomain, dnsRecordID) - .then((dnsRecordUpdate) => { - console.log(dnsRecordUpdate); - return NginxServerBlock(subdomain, port); - }) - .catch((error) => { - console.error(`Error updating DNS record: ${error}`); - throw new Error('Error setting up subdomain'); - }) - .then((serverBlockCreated) => { - console.log(serverBlockCreated); - return NginxServerBlockSymLink(subdomain); - }) - .catch((error) => { - console.error(`Error creating NGINX server block: ${error}`); - throw new Error('Error setting up subdomain'); - }) - .then((symlinkCreated) => { - console.log(symlinkCreated); - return getSSL(subdomain); - }) - .catch((error) => { - console.error(`Error creating symbolic link: ${error}`); - throw new Error('Error setting up subdomain'); - }) - .then((sslObtained) => { - console.log(sslObtained); - return restartNginx(); - }) - .catch((error) => { - console.error(`Error obtaining SSL certificate: ${error}`); - throw new Error('Error setting up subdomain'); - }) - .then((nginxRestarted) => { - console.log(nginxRestarted); - return 'Subdomain setup completed'; - }) - .catch((error) => { - console.error(`Error restarting NGINX: ${error}`); - throw new Error('Error setting up subdomain'); - }); -} diff --git a/src/app.module.ts b/src/app.module.ts new file mode 100644 index 0000000..38f7227 --- /dev/null +++ b/src/app.module.ts @@ -0,0 +1,25 @@ +import { Module } from '@nestjs/common'; +import { ServeStaticModule } from '@nestjs/serve-static'; +import { ConfigModule } from './config/config.module'; +import { AuthModule } from './auth/auth.module'; +import { ContainerModule } from './container/container.module'; +import { DeploymentModule } from './deployment/deployment.module'; +import { DatabaseModule } from './db/database.module'; +import { SubdomainModule } from './subdomain/subdomain.module'; +import { HtmxModule } from './htmx/htmx.module'; +import * as path from 'path'; +@Module({ + imports: [ + ServeStaticModule.forRoot({ + rootPath: '/home/akshat/projects/cloudRun/frontend/dist', // Path to your frontend directory + }), + ConfigModule, + AuthModule, + ContainerModule, + DeploymentModule, + DatabaseModule, + SubdomainModule, + HtmxModule, + ], +}) +export class AppModule {} diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts new file mode 100644 index 0000000..3826a16 --- /dev/null +++ b/src/auth/auth.controller.ts @@ -0,0 +1,20 @@ +// src/auth/auth.controller.ts +import { Controller, Post, Req, Res, UseGuards } from '@nestjs/common'; +import { Request, Response } from 'express'; +import { AuthService } from './auth.service'; + +@Controller('auth') +export class AuthController { + constructor(private readonly authService: AuthService) {} + + @Post('login') + async login(@Req() req: Request, @Res() res: Response) { + const { passKey } = req.body; + const sessionId = await this.authService.login(passKey); + if (sessionId) { + res.cookie('sessionId', sessionId, { httpOnly: true, secure: false }); + return res.status(200).json({ message: 'Login successful' }); + } + return res.status(401).json({ message: 'Unauthorized' }); + } +} diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts new file mode 100644 index 0000000..39afe0f --- /dev/null +++ b/src/auth/auth.module.ts @@ -0,0 +1,10 @@ +// src/auth/auth.module.ts +import { Module } from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { AuthController } from './auth.controller'; + +@Module({ + providers: [AuthService], + controllers: [AuthController], +}) +export class AuthModule {} diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts new file mode 100644 index 0000000..587d0a3 --- /dev/null +++ b/src/auth/auth.service.ts @@ -0,0 +1,13 @@ +// src/auth/auth.service.ts +import { Injectable } from '@nestjs/common'; +import { createDirectory } from '../utils/container.util'; + +@Injectable() +export class AuthService { + login(passKey: string): string | null { + if (passKey === process.env.PASS_KEY) { + return Math.random().toString(36).substring(2); // Generate session ID + } + return null; + } +} diff --git a/src/config/config.module.ts b/src/config/config.module.ts new file mode 100644 index 0000000..771746a --- /dev/null +++ b/src/config/config.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { + ConfigModule as NestConfigModule, + ConfigService, +} from '@nestjs/config'; + +@Module({ + imports: [ + NestConfigModule.forRoot({ + isGlobal: true, // Makes ConfigService globally available + envFilePath: ['.env'], // Load environment variables from .env + }), + ], + providers: [ConfigService], // Register ConfigService as a provider + exports: [ConfigService], // Export ConfigService for use in other modules +}) +export class ConfigModule {} diff --git a/src/container/container.controller.ts b/src/container/container.controller.ts new file mode 100644 index 0000000..48a5925 --- /dev/null +++ b/src/container/container.controller.ts @@ -0,0 +1,21 @@ +// src/container/container.controller.ts +import { Controller, Post, Body, Res } from '@nestjs/common'; +import { Response } from 'express'; +import { ContainerService } from './container.service'; + +@Controller('container') +export class ContainerController { + constructor(private readonly containerService: ContainerService) {} + + @Post('run') + async runContainer(@Body() body: any, @Res() res: Response) { + try { + const result = await this.containerService.runContainer(body); + res.json(result); + } catch (error) { + res + .status(500) + .json({ error: 'Deployment failed', message: error.message }); + } + } +} diff --git a/src/container/container.module.ts b/src/container/container.module.ts new file mode 100644 index 0000000..30f357c --- /dev/null +++ b/src/container/container.module.ts @@ -0,0 +1,13 @@ +// src/container/container.module.ts +import { Module } from '@nestjs/common'; +import { ContainerService } from './container.service'; +import { ContainerController } from './container.controller'; +import { SubdomainModule } from '../subdomain/subdomain.module'; +import { DeploymentModule } from '../deployment/deployment.module'; +import { ConfigService } from '@nestjs/config'; +@Module({ + imports: [DeploymentModule, SubdomainModule], + providers: [ContainerService, ConfigService], + controllers: [ContainerController], +}) +export class ContainerModule {} diff --git a/src/container/container.service.ts b/src/container/container.service.ts new file mode 100644 index 0000000..007963f --- /dev/null +++ b/src/container/container.service.ts @@ -0,0 +1,121 @@ +import { Injectable } from '@nestjs/common'; +import { DeploymentService } from '../deployment/deployment.service'; +import { SubdomainService } from '../subdomain/subdomain.service'; +import { getPort, dockerFile, createDirectory } from '../utils/container.util'; +import { promisify } from 'util'; +import { exec } from 'child_process'; + +const execAsync = promisify(exec); + +@Injectable() +export class ContainerService { + constructor( + private readonly deploymentService: DeploymentService, + private readonly subdomainService: SubdomainService + ) {} + + async runContainer(body: any) { + const { + userName, + projectName, + repoLink, + entryPoint, + buildCommand = 'npm install', + runCommand = 'node', + } = body; + + // Validate required fields + const missingFields: string[] = []; // Explicitly type as string[] + if (!userName) missingFields.push('userName'); + if (!projectName) missingFields.push('projectName'); + if (!repoLink) missingFields.push('repoLink'); + if (!entryPoint) missingFields.push('entryPoint'); + + if (missingFields.length > 0) { + throw new Error(`Missing required fields: ${missingFields.join(', ')}`); + } + + try { + // Track deployment limit + await this.deploymentService.addDeployment(projectName, ''); // Use addDeployment instead of trackDeployment + + // Create directory for user + createDirectory(userName); + + // Generate Dockerfile and build image + const imageName = `${userName.toLowerCase()}-${projectName}`; + const port = await this.findAvailablePort(); + await this.createImage( + userName, + projectName, + repoLink, + entryPoint, + buildCommand, + runCommand + ); + + // Run container + const { stdout } = await execAsync( + `podman run -d -p ${port}:8080 -t localhost/${imageName}:latest` + ); + const containerId = stdout.trim(); + + // Set up subdomain + const subdomain = containerId.substring(0, 12); + await this.subdomainService.setupSubdomain(subdomain, port, containerId); + + return { containerId, imageName, status: 'deployed' }; + } catch (error) { + console.error('Error deploying container:', error); + throw new Error('Deployment failed'); + } + } + + private async findAvailablePort(): Promise { + // Use utility function to find an available port + return getPort(8081); + } + + private async createImage( + userName: string, + projectName: string, + repoLink: string, + entryPoint: string, + buildCommand: string, + runCommand: string + ): Promise { + try { + // Change directory to user folder + process.chdir( + `/home/${process.env.LINUX_USER || 'root'}/${userName.toLowerCase()}` + ); + + // Clone repository into project directory + await execAsync(`git clone ${repoLink}`); + process.chdir(projectName); + + // Check if Dockerfile exists; create if not + try { + await execAsync('ls Dockerfile'); + console.log('Dockerfile already exists, skipping creation.'); + } catch { + const dockerfileContent = dockerFile( + entryPoint, + buildCommand, + runCommand + ); + await execAsync(`echo '${dockerfileContent}' > Dockerfile`); + console.log('Dockerfile created.'); + } + + // Build Docker image + await execAsync( + `buildah build -t ${userName.toLowerCase()}-${projectName} .` + ); + console.log('Docker image built successfully.'); + } catch (error) { + console.error('Error creating Docker image:', error); + throw error; + } + } +} diff --git a/src/db/database.module.ts b/src/db/database.module.ts new file mode 100644 index 0000000..6472fa1 --- /dev/null +++ b/src/db/database.module.ts @@ -0,0 +1,23 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Deployment } from '../deployment/deployment.entity'; +import { ConfigService } from '@nestjs/config'; + +@Module({ + imports: [ + TypeOrmModule.forRootAsync({ + useFactory: (configService: ConfigService) => ({ + type: 'postgres' as const, // Explicitly type as "postgres" + host: configService.get('PG_HOST', 'localhost'), + port: configService.get('PG_PORT', 5444), + username: configService.get('PG_USERNAME', 'postgres'), + password: configService.get('PG_PASSWORD', 'password'), + database: configService.get('PG_DATABASE', 'flexhost'), + entities: [Deployment], + synchronize: true, + }), + inject: [ConfigService], + }), + ], +}) +export class DatabaseModule {} diff --git a/src/deployment/deployment.controller.ts b/src/deployment/deployment.controller.ts new file mode 100644 index 0000000..b1a4a60 --- /dev/null +++ b/src/deployment/deployment.controller.ts @@ -0,0 +1,13 @@ +// src/deployment/deployment.controller.ts +import { Controller, Get, Query } from '@nestjs/common'; +import { DeploymentService } from './deployment.service'; + +@Controller('deployment') +export class DeploymentController { + constructor(private readonly deploymentService: DeploymentService) {} + + @Get() + async getDeployments() { + return this.deploymentService.getDeployments(); + } +} diff --git a/src/deployment/deployment.entity.ts b/src/deployment/deployment.entity.ts new file mode 100644 index 0000000..4eefecf --- /dev/null +++ b/src/deployment/deployment.entity.ts @@ -0,0 +1,19 @@ +import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; + +@Entity() +export class Deployment { + @PrimaryGeneratedColumn() + id: number; + + @Column() + projectName: string; + + @Column() + status: string; + + @Column() + time: Date; + + @Column() + containerId: string; +} diff --git a/src/deployment/deployment.module.ts b/src/deployment/deployment.module.ts new file mode 100644 index 0000000..96a7569 --- /dev/null +++ b/src/deployment/deployment.module.ts @@ -0,0 +1,14 @@ +// src/deployment/deployment.module.ts +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { DeploymentService } from './deployment.service'; +import { DeploymentController } from './deployment.controller'; +import { Deployment } from './deployment.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Deployment])], + providers: [DeploymentService], + controllers: [DeploymentController], + exports: [DeploymentService], +}) +export class DeploymentModule {} diff --git a/src/deployment/deployment.service.ts b/src/deployment/deployment.service.ts new file mode 100644 index 0000000..31b87be --- /dev/null +++ b/src/deployment/deployment.service.ts @@ -0,0 +1,26 @@ +// src/deployment/deployment.service.ts +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Deployment } from './deployment.entity'; + +@Injectable() +export class DeploymentService { + constructor( + @InjectRepository(Deployment) + private readonly deploymentRepository: Repository + ) {} + + async getDeployments() { + return this.deploymentRepository.find(); + } + + async addDeployment(projectName: string, containerId: string) { + const deployment = new Deployment(); + deployment.projectName = projectName; + deployment.status = 'deployed'; + deployment.time = new Date(); + deployment.containerId = containerId; + await this.deploymentRepository.save(deployment); + } +} diff --git a/src/htmx/htmx.controller.ts b/src/htmx/htmx.controller.ts new file mode 100644 index 0000000..3a0c101 --- /dev/null +++ b/src/htmx/htmx.controller.ts @@ -0,0 +1,149 @@ +// src/htmx/htmx.controller.ts +import { + Controller, + Post, + Get, + Query, + Body, + Res, + HttpStatus, +} from '@nestjs/common'; +import { Response } from 'express'; +import { HtmxService } from './htmx.service'; +import { ConfigService } from '@nestjs/config'; + +@Controller('htmx') +export class HtmxController { + public nickname: string; + constructor( + private readonly htmxService: HtmxService, + private readonly configService: ConfigService + ) { + this.nickname = this.configService.get( + 'GH_USERNAME', + 'IntegerAlex' + ); + } + + @Post() + async runContainer(@Body() body: any, @Res() res: Response) { + const { userName, buildCommand, runCommand, repoLink, entryPoint } = body; + try { + const result = await this.htmxService.runContainer( + userName, + repoLink, + entryPoint, + buildCommand, + runCommand + ); + res.status(HttpStatus.OK).json({ + success: true, + data: { containerId: result.containerId }, + }); + } catch (error) { + console.error('Error:', error); + res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ + success: false, + error: 'Error deploying container', + }); + } + } + + @Get('deployments') + async getDeployments( + @Query('userName') userName: string, + @Res() res: Response + ) { + try { + const deployments = await this.htmxService.getDeployments(); + if (!deployments || deployments.length === 0) { + return res.status(HttpStatus.OK).json({ + success: true, + message: 'No deployments found', + }); + } + const formattedDeployments = deployments.map((deployment) => ({ + projectName: deployment.projectName, + status: deployment.status, + deployedAt: new Date(deployment.time).toLocaleString(), + containerId: deployment.containerId, + })); + res.status(HttpStatus.OK).json({ + success: true, + data: formattedDeployments, + }); + } catch (error) { + console.error('Error fetching deployments:', error); + res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ + success: false, + error: 'Error fetching deployments', + }); + } + } + + @Get('subscription') + async getSubscription( + @Query('userName') userName: string, + @Res() res: Response + ) { + try { + const subscription = await this.htmxService.getSubscription( + userName.toLowerCase() + ); + const message = subscription + ? 'You are in free tier' + : 'You can deploy one application'; + res.status(HttpStatus.OK).json({ + success: true, + data: { + tier: 'free', + message, + upgrade: subscription ? 'Upgrade to premium' : null, + }, + }); + } catch (error) { + console.error('Error fetching subscription:', error); + res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ + success: false, + error: 'Error fetching subscription', + }); + } + } + + @Get('v1/profile') + async getProfile(@Res() res: Response) { + res.status(HttpStatus.OK).json({ + success: true, + data: { nickname: this.nickname }, + }); + } + + @Get('v1/repositories') + async getRepositories(@Res() res: Response) { + try { + const response = await fetch( + `https://api.github.com/users/${this.nickname}/repos`, + { method: 'GET' } + ); + const data = await response.json(); + const repositories = data.map((repo: any) => ({ + name: repo.name, + url: repo.html_url, + })); + const avatarUrl = data[0]?.owner?.avatar_url; + res.status(HttpStatus.OK).json({ + success: true, + data: { + repositories, + avatarUrl, + }, + }); + } catch (error) { + console.error('Error fetching repositories:', error); + res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ + success: false, + error: 'Error fetching repositories', + }); + } + } +} diff --git a/src/htmx/htmx.module.ts b/src/htmx/htmx.module.ts new file mode 100644 index 0000000..8829429 --- /dev/null +++ b/src/htmx/htmx.module.ts @@ -0,0 +1,12 @@ +// src/htmx/htmx.module.ts +import { Module } from '@nestjs/common'; +import { HtmxService } from './htmx.service'; +import { HtmxController } from './htmx.controller'; +import { DeploymentModule } from '../deployment/deployment.module'; + +@Module({ + imports: [DeploymentModule], + providers: [HtmxService], + controllers: [HtmxController], +}) +export class HtmxModule {} diff --git a/src/htmx/htmx.service.ts b/src/htmx/htmx.service.ts new file mode 100644 index 0000000..29e03b0 --- /dev/null +++ b/src/htmx/htmx.service.ts @@ -0,0 +1,45 @@ +// src/htmx/htmx.service.ts +import { Injectable } from '@nestjs/common'; +import { DeploymentService } from '../deployment/deployment.service'; + +@Injectable() +export class HtmxService { + constructor(private readonly deploymentService: DeploymentService) {} + + async runContainer( + userName: string, + repoLink: string, + entryPoint: string, + buildCommand?: string, + runCommand?: string + ) { + const projectName = repoLink.split('/').pop()?.split('.')[0] || ''; + const response = await fetch('http://localhost:8080/v1/runContainer', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + userName, + projectName, + repoLink, + buildCommand: buildCommand || 'npm install', + runCommand: runCommand || 'node', + entryPoint, + }), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.message || 'Error occurred'); + } + + return response.json(); + } + + async getDeployments() { + return this.deploymentService.getDeployments(); + } + + async getSubscription(userName: string) { + return true; + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..8890efc --- /dev/null +++ b/src/main.ts @@ -0,0 +1,11 @@ +// src/main.ts +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + app.enableCors(); + await app.listen(8080); + console.log('Server is running on port 8080'); +} +bootstrap(); diff --git a/src/subdomain/subdomain.controller.ts b/src/subdomain/subdomain.controller.ts new file mode 100644 index 0000000..807e225 --- /dev/null +++ b/src/subdomain/subdomain.controller.ts @@ -0,0 +1,26 @@ +// src/subdomain/subdomain.controller.ts +import { Controller, Post, Body, Res } from '@nestjs/common'; +import { Response } from 'express'; +import { SubdomainService } from './subdomain.service'; + +@Controller('subdomain') +export class SubdomainController { + constructor(private readonly subdomainService: SubdomainService) {} + + @Post('setup') + async setupSubdomain(@Body() body: any, @Res() res: Response) { + const { subdomain, port, dnsRecordId } = body; + try { + const result = await this.subdomainService.setupSubdomain( + subdomain, + port, + dnsRecordId + ); + res.json({ message: result }); + } catch (error) { + res + .status(500) + .json({ error: 'Subdomain setup failed', message: error.message }); + } + } +} diff --git a/src/subdomain/subdomain.module.ts b/src/subdomain/subdomain.module.ts new file mode 100644 index 0000000..496c932 --- /dev/null +++ b/src/subdomain/subdomain.module.ts @@ -0,0 +1,11 @@ +// src/subdomain/subdomain.module.ts +import { Module } from '@nestjs/common'; +import { SubdomainService } from './subdomain.service'; +import { SubdomainController } from './subdomain.controller'; + +@Module({ + providers: [SubdomainService], + controllers: [SubdomainController], + exports: [SubdomainService], // Export SubdomainService if needed in other modules +}) +export class SubdomainModule {} diff --git a/src/subdomain/subdomain.service.ts b/src/subdomain/subdomain.service.ts new file mode 100644 index 0000000..56f5c3b --- /dev/null +++ b/src/subdomain/subdomain.service.ts @@ -0,0 +1,96 @@ +// src/subdomain/subdomain.service.ts +import { Injectable } from '@nestjs/common'; +import { exec } from 'child_process'; +import { promisify } from 'util'; +import * as fs from 'fs'; + +const execAsync = promisify(exec); + +@Injectable() +export class SubdomainService { + async setupSubdomain( + subdomain: string, + port: number, + dnsRecordId: string + ): Promise { + await this.addDnsRecord(subdomain, dnsRecordId); + await this.createNginxConfig(subdomain, port); + await this.createNginxSymlink(subdomain); + await this.getSSL(subdomain); + await this.restartNginx(); + return 'Subdomain setup completed'; + } + + private async addDnsRecord( + subdomain: string, + dnsRecordId: string + ): Promise { + const zoneId = process.env.CLOUDFLARE_ZONE_ID; + const url = `https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records`; + const data = { + type: 'A', + name: `${subdomain}.flexr`, + content: '35.223.20.186', + ttl: 120, + proxied: false, + comment: 'Domain verification record', + tags: [], + id: dnsRecordId, + }; + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Auth-Email': process.env.CLOUDFLARE_EMAIL || '', + 'X-Auth-Key': process.env.CLOUDFLARE_GLOBAL_TOKEN || '', + }, + body: JSON.stringify(data), + }); + + if (!response.ok) { + throw new Error('Error updating DNS record'); + } + } + + private async createNginxConfig( + subdomain: string, + port: number + ): Promise { + const config = ` +server { + listen 443 ssl; + server_name ${subdomain}.flexr.flexhost.tech; + ssl_certificate /etc/letsencrypt/live/${subdomain}.flexr.flexhost.tech/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/${subdomain}.flexr.flexhost.tech/privkey.pem; + location / { + proxy_pass http://localhost:${port}; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + `; + + fs.writeFileSync(`/etc/nginx/sites-available/${subdomain}`, config, { + encoding: 'utf8', + }); + } + + private async createNginxSymlink(subdomain: string): Promise { + await execAsync( + `sudo ln -s /etc/nginx/sites-available/${subdomain} /etc/nginx/sites-enabled/` + ); + } + + private async getSSL(subdomain: string): Promise { + await execAsync( + `sudo certbot --nginx -d ${subdomain}.flexr.flexhost.tech --non-interactive --agree-tos --email ${process.env.CERTBOT_EMAIL} && sudo systemctl reload nginx` + ); + } + + private async restartNginx(): Promise { + await execAsync('sudo systemctl reload nginx'); + } +} diff --git a/src/utils/container.util.ts b/src/utils/container.util.ts new file mode 100644 index 0000000..d8f816b --- /dev/null +++ b/src/utils/container.util.ts @@ -0,0 +1,34 @@ +import * as fs from 'fs'; +import * as net from 'net'; + +export const dockerFile = ( + entryPoint: string, + buildCommand: string, + runCommand: string +): string => ` +FROM node:22-alpine +WORKDIR /app +COPY . . +RUN npm install && ${buildCommand} +EXPOSE 8080 +CMD ["${runCommand}", "${entryPoint}"] +`; + +export async function getPort(findPort: number): Promise { + const server = net.createServer(); + return new Promise((resolve) => { + server.once('error', (err) => + resolve(err.message === 'EADDRINUSE' ? getPort(findPort + 1) : findPort) + ); + server.once('listening', () => server.close(() => resolve(findPort))); + server.listen(findPort); + }); +} + +export function createDirectory(userName: string): void { + const linuxUser = process.env.LINUX_USER || 'root'; + const dirPath = `/home/${linuxUser}/${userName.toLowerCase()}`; + if (!fs.existsSync(dirPath)) { + fs.mkdirSync(dirPath, { recursive: true }); + } +} diff --git a/src/utils/server.util.ts b/src/utils/server.util.ts new file mode 100644 index 0000000..fcd2a41 --- /dev/null +++ b/src/utils/server.util.ts @@ -0,0 +1,92 @@ +import * as fs from 'fs'; +import { exec } from 'child_process'; +import { promisify } from 'util'; + +const execAsync = promisify(exec); + +export async function addRecord( + subdomain: string, + dnsRecordId: string +): Promise { + const zoneId = process.env.CLOUDFLARE_ZONE_ID; + const url = `https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records`; + const data = { + type: 'A', + name: `${subdomain}.flexr`, + content: '35.223.20.186', + ttl: 120, + proxied: false, + comment: 'Domain verification record', + tags: [], + id: dnsRecordId, + }; + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Auth-Email': process.env.CLOUDFLARE_EMAIL || '', + 'X-Auth-Key': process.env.CLOUDFLARE_GLOBAL_TOKEN || '', + }, + body: JSON.stringify(data), + }); + + if (!response.ok) { + throw new Error('Error updating DNS record'); + } + + return 'DNS record updated'; +} + +export async function setupSubdomain( + subdomain: string, + port: number, + dnsRecordId: string +): Promise { + await addRecord(subdomain, dnsRecordId); + await createNginxConfig(subdomain, port); + await createNginxSymlink(subdomain); + await getSSL(subdomain); + await restartNginx(); + return 'Subdomain setup completed'; +} + +async function createNginxConfig( + subdomain: string, + port: number +): Promise { + const config = ` +server { + listen 443 ssl; + server_name ${subdomain}.flexr.flexhost.tech; + ssl_certificate /etc/letsencrypt/live/${subdomain}.flexr.flexhost.tech/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/${subdomain}.flexr.flexhost.tech/privkey.pem; + location / { + proxy_pass http://localhost:${port}; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + `; + fs.writeFileSync(`/etc/nginx/sites-available/${subdomain}`, config, { + encoding: 'utf8', + }); +} + +async function createNginxSymlink(subdomain: string): Promise { + await execAsync( + `sudo ln -s /etc/nginx/sites-available/${subdomain} /etc/nginx/sites-enabled/` + ); +} + +async function getSSL(subdomain: string): Promise { + await execAsync( + `sudo certbot --nginx -d ${subdomain}.flexr.flexhost.tech --non-interactive --agree-tos --email ${process.env.CERTBOT_EMAIL}` + ); +} + +async function restartNginx(): Promise { + await execAsync('sudo systemctl reload nginx'); +} diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..64f86c6 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] +} diff --git a/tsconfig.json b/tsconfig.json index e2ddb9c..e4dbf2e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,30 +1,21 @@ { "compilerOptions": { - "target": "ES2020", "module": "commonjs", - "lib": ["es2020"], - "strict": true, - "esModuleInterop": true, + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2023", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, "skipLibCheck": true, + "strictNullChecks": true, "forceConsistentCasingInFileNames": true, - "outDir": "dist", - "rootDir": ".", - "sourceMap": true, - "declaration": true, - "resolveJsonModule": true, - "moduleResolution": "node", - "baseUrl": ".", - "paths": { - "*": ["node_modules/*"] - } - }, - "include": [ - "server/**/*", - "db/**/*" - ], - "exclude": [ - "node_modules", - "dist" - ] + "noImplicitAny": false, + "strictBindCallApply": false, + "noFallthroughCasesInSwitch": false + } } -