From 6749e343ba77709c632c52bf09502f433402d677 Mon Sep 17 00:00:00 2001 From: Louis Lam Date: Sat, 11 Nov 2023 22:18:37 +0800 Subject: [PATCH] Init (#1) --- .dockerignore | 17 + .editorconfig | 24 + .eslintrc.cjs | 97 + .github/FUNDING.yml | 12 + .gitignore | 12 + LICENSE | 21 + README.md | 130 + backend/check-version.ts | 71 + backend/database.ts | 247 + backend/docker.ts | 3 + backend/dockge-server.ts | 559 ++ backend/index.ts | 6 + backend/log.ts | 208 + .../2023-10-20-0829-setting-table.ts | 14 + .../migrations/2023-10-20-0829-user-table.ts | 19 + backend/models/user.ts | 46 + backend/password-hash.ts | 47 + backend/rate-limiter.ts | 75 + backend/router.ts | 6 + backend/routers/main-router.ts | 23 + backend/settings.ts | 174 + backend/socket-handler.ts | 6 + .../socket-handlers/docker-socket-handler.ts | 262 + .../socket-handlers/main-socket-handler.ts | 295 ++ .../terminal-socket-handler.ts | 151 + backend/stack.ts | 356 ++ backend/terminal.ts | 230 + backend/util-common.ts | 337 ++ backend/util-server.ts | 79 + backend/utils/limit-queue.ts | 24 + compose.yaml | 18 + docker/Base.Dockerfile | 39 + docker/Dockerfile | 29 + extra/mark-as-nightly.ts | 22 + extra/templates/mariadb/compose.yaml | 9 + .../nginx-proxy-manager/compose.yaml | 12 + extra/templates/uptime-kuma/compose.yaml | 9 + frontend/components.d.ts | 30 + frontend/index.html | 33 + frontend/public/apple-touch-icon.png | Bin 0 -> 10438 bytes frontend/public/favicon.ico | Bin 0 -> 5430 bytes frontend/public/icon-192x192.png | Bin 0 -> 9624 bytes frontend/public/icon-512x512.png | Bin 0 -> 28382 bytes frontend/public/icon.svg | 14 + frontend/public/manifest.json | 19 + frontend/src/App.vue | 9 + frontend/src/components/ArrayInput.vue | 117 + frontend/src/components/ArraySelect.vue | 125 + frontend/src/components/Confirm.vue | 84 + frontend/src/components/Container.vue | 273 + frontend/src/components/HiddenInput.vue | 87 + frontend/src/components/Login.vue | 114 + frontend/src/components/NetworkInput.vue | 223 + frontend/src/components/StackList.vue | 438 ++ frontend/src/components/StackListItem.vue | 154 + frontend/src/components/Terminal.vue | 228 + frontend/src/components/TwoFADialog.vue | 203 + frontend/src/components/Uptime.vue | 54 + frontend/src/components/settings/About.vue | 66 + .../src/components/settings/Appearance.vue | 94 + frontend/src/components/settings/General.vue | 114 + frontend/src/components/settings/Security.vue | 205 + frontend/src/i18n.ts | 36 + frontend/src/icon.ts | 115 + frontend/src/lang/en.json | 53 + frontend/src/layouts/EmptyLayout.vue | 8 + frontend/src/layouts/Layout.vue | 304 ++ frontend/src/main.ts | 101 + frontend/src/mixins/lang.ts | 39 + frontend/src/mixins/socket.ts | 317 ++ frontend/src/mixins/theme.ts | 80 + frontend/src/pages/Compose.vue | 599 +++ frontend/src/pages/Console.vue | 48 + frontend/src/pages/ContainerTerminal.vue | 63 + frontend/src/pages/Dashboard.vue | 42 + frontend/src/pages/DashboardHome.vue | 231 + frontend/src/pages/Settings.vue | 252 + frontend/src/pages/Setup.vue | 138 + frontend/src/router.ts | 90 + frontend/src/styles/localization.scss | 9 + frontend/src/styles/main.scss | 697 +++ frontend/src/styles/vars.scss | 26 + frontend/src/util-frontend.ts | 215 + frontend/src/vite-env.d.ts | 7 + frontend/vite.config.ts | 36 + package.json | 79 + pnpm-lock.yaml | 4477 +++++++++++++++++ tsconfig.json | 8 + 88 files changed, 14443 insertions(+) create mode 100644 .dockerignore create mode 100644 .editorconfig create mode 100644 .eslintrc.cjs create mode 100644 .github/FUNDING.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 backend/check-version.ts create mode 100644 backend/database.ts create mode 100644 backend/docker.ts create mode 100644 backend/dockge-server.ts create mode 100644 backend/index.ts create mode 100644 backend/log.ts create mode 100644 backend/migrations/2023-10-20-0829-setting-table.ts create mode 100644 backend/migrations/2023-10-20-0829-user-table.ts create mode 100644 backend/models/user.ts create mode 100644 backend/password-hash.ts create mode 100644 backend/rate-limiter.ts create mode 100644 backend/router.ts create mode 100644 backend/routers/main-router.ts create mode 100644 backend/settings.ts create mode 100644 backend/socket-handler.ts create mode 100644 backend/socket-handlers/docker-socket-handler.ts create mode 100644 backend/socket-handlers/main-socket-handler.ts create mode 100644 backend/socket-handlers/terminal-socket-handler.ts create mode 100644 backend/stack.ts create mode 100644 backend/terminal.ts create mode 100644 backend/util-common.ts create mode 100644 backend/util-server.ts create mode 100644 backend/utils/limit-queue.ts create mode 100644 compose.yaml create mode 100644 docker/Base.Dockerfile create mode 100644 docker/Dockerfile create mode 100644 extra/mark-as-nightly.ts create mode 100644 extra/templates/mariadb/compose.yaml create mode 100644 extra/templates/nginx-proxy-manager/compose.yaml create mode 100644 extra/templates/uptime-kuma/compose.yaml create mode 100644 frontend/components.d.ts create mode 100644 frontend/index.html create mode 100644 frontend/public/apple-touch-icon.png create mode 100644 frontend/public/favicon.ico create mode 100644 frontend/public/icon-192x192.png create mode 100644 frontend/public/icon-512x512.png create mode 100644 frontend/public/icon.svg create mode 100644 frontend/public/manifest.json create mode 100644 frontend/src/App.vue create mode 100644 frontend/src/components/ArrayInput.vue create mode 100644 frontend/src/components/ArraySelect.vue create mode 100644 frontend/src/components/Confirm.vue create mode 100644 frontend/src/components/Container.vue create mode 100644 frontend/src/components/HiddenInput.vue create mode 100644 frontend/src/components/Login.vue create mode 100644 frontend/src/components/NetworkInput.vue create mode 100644 frontend/src/components/StackList.vue create mode 100644 frontend/src/components/StackListItem.vue create mode 100644 frontend/src/components/Terminal.vue create mode 100644 frontend/src/components/TwoFADialog.vue create mode 100644 frontend/src/components/Uptime.vue create mode 100644 frontend/src/components/settings/About.vue create mode 100644 frontend/src/components/settings/Appearance.vue create mode 100644 frontend/src/components/settings/General.vue create mode 100644 frontend/src/components/settings/Security.vue create mode 100644 frontend/src/i18n.ts create mode 100644 frontend/src/icon.ts create mode 100644 frontend/src/lang/en.json create mode 100644 frontend/src/layouts/EmptyLayout.vue create mode 100644 frontend/src/layouts/Layout.vue create mode 100644 frontend/src/main.ts create mode 100644 frontend/src/mixins/lang.ts create mode 100644 frontend/src/mixins/socket.ts create mode 100644 frontend/src/mixins/theme.ts create mode 100644 frontend/src/pages/Compose.vue create mode 100644 frontend/src/pages/Console.vue create mode 100644 frontend/src/pages/ContainerTerminal.vue create mode 100644 frontend/src/pages/Dashboard.vue create mode 100644 frontend/src/pages/DashboardHome.vue create mode 100644 frontend/src/pages/Settings.vue create mode 100644 frontend/src/pages/Setup.vue create mode 100644 frontend/src/router.ts create mode 100644 frontend/src/styles/localization.scss create mode 100644 frontend/src/styles/main.scss create mode 100644 frontend/src/styles/vars.scss create mode 100644 frontend/src/util-frontend.ts create mode 100644 frontend/src/vite-env.d.ts create mode 100644 frontend/vite.config.ts create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 tsconfig.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1adf5fe --- /dev/null +++ b/.dockerignore @@ -0,0 +1,17 @@ +# Should be identical to .gitignore +.env +node_modules +.idea +data +stacks +tmp +/private + +# Docker extra +docker +frontend +.editorconfig +.eslintrc.cjs +.git +.gitignore +README.md diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..47bf476 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,24 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false + +[*.yaml] +indent_size = 2 + +[*.yml] +indent_size = 2 + +[*.vue] +trim_trailing_whitespace = false + +[*.go] +indent_style = tab diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..1a292dc --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,97 @@ +module.exports = { + root: true, + env: { + browser: true, + node: true, + }, + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:vue/vue3-recommended", + ], + parser: "vue-eslint-parser", + parserOptions: { + "parser": "@typescript-eslint/parser", + }, + plugins: [ + "@typescript-eslint", + "jsdoc" + ], + rules: { + "yoda": "error", + "linebreak-style": [ "error", "unix" ], + "camelcase": [ "warn", { + "properties": "never", + "ignoreImports": true + }], + "no-unused-vars": [ "warn", { + "args": "none" + }], + indent: [ + "error", + 4, + { + ignoredNodes: [ "TemplateLiteral" ], + SwitchCase: 1, + }, + ], + quotes: [ "error", "double" ], + semi: "error", + "vue/html-indent": [ "error", 4 ], // default: 2 + "vue/max-attributes-per-line": "off", + "vue/singleline-html-element-content-newline": "off", + "vue/html-self-closing": "off", + "vue/require-component-is": "off", // not allow is="style" https://github.com/vuejs/eslint-plugin-vue/issues/462#issuecomment-430234675 + "vue/attribute-hyphenation": "off", // This change noNL to "no-n-l" unexpectedly + "vue/multi-word-component-names": "off", + "no-multi-spaces": [ "error", { + ignoreEOLComments: true, + }], + "array-bracket-spacing": [ "warn", "always", { + "singleValue": true, + "objectsInArrays": false, + "arraysInArrays": false + }], + "space-before-function-paren": [ "error", { + "anonymous": "always", + "named": "never", + "asyncArrow": "always" + }], + "curly": "error", + "object-curly-spacing": [ "error", "always" ], + "object-curly-newline": "off", + "object-property-newline": "error", + "comma-spacing": "error", + "brace-style": "error", + "no-var": "error", + "key-spacing": "warn", + "keyword-spacing": "warn", + "space-infix-ops": "error", + "arrow-spacing": "warn", + "no-trailing-spaces": "error", + "no-constant-condition": [ "error", { + "checkLoops": false, + }], + "space-before-blocks": "warn", + "no-extra-boolean-cast": "off", + "no-multiple-empty-lines": [ "warn", { + "max": 1, + "maxBOF": 0, + }], + "lines-between-class-members": [ "warn", "always", { + exceptAfterSingleLine: true, + }], + "no-unneeded-ternary": "error", + "array-bracket-newline": [ "error", "consistent" ], + "eol-last": [ "error", "always" ], + "comma-dangle": [ "warn", "only-multiline" ], + "no-empty": [ "error", { + "allowEmptyCatch": true + }], + "no-control-regex": "off", + "one-var": [ "error", "never" ], + "max-statements-per-line": [ "error", { "max": 1 }], + "@typescript-eslint/ban-ts-comment": "off", + "prefer-const" : "off", + }, +}; diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..d55fbd4 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: louislam # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +#patreon: # Replace with a single Patreon username +open_collective: uptime-kuma # Replace with a single Open Collective username +#ko_fi: # Replace with a single Ko-fi username +#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +#liberapay: # Replace with a single Liberapay username +#issuehunt: # Replace with a single IssueHunt username +#otechie: # Replace with a single Otechie username +#custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..decd817 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Should update .dockerignore as well +.env +node_modules +.idea +data +stacks +tmp +/private + +# Git only +frontend-dist + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a954450 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Louis Lam + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index cd7e17a..e5b8615 100644 --- a/README.md +++ b/README.md @@ -1 +1,131 @@ +
+ +
+ # Dockge + +A fancy, easy-to-use and reactive docker `compose.yaml` stack-oriented manager. + + + +## ⭐ Features + +- Manage `compose.yaml` +- Interactive Editor for `compose.yaml` +- Interactive Web Terminal +- Reactive + - Everything is just responsive. Progress (Pull/Up/Down) and terminal output are in real-time +- Easy-to-use & fancy UI + - If you love Uptime Kuma's UI/UX, you will love this too +- Convert `docker run ...` commands into `compose.yaml` + +## 🔧 How to Install + +Requirements: +- [Docker CE](https://docs.docker.com/engine/install/) 20+ is recommended +- [Docker Compose V2](https://docs.docker.com/compose/install/linux/) +- OS: + - As long as you can run Docker CE, it should be fine, but: + - Debian/Raspbian Buster or lower is not supported, please upgrade to Bullseye +- Arch: armv7, arm64, amd64 (a.k.a x86_64) + +### Basic + +Default stacks directory is `/opt/stacks`. + +``` +# Create a directory that stores your stacks and stores dockge's compose.yaml +mkdir -p /opt/stacks /opt/dockge +cd /opt/dockge + +# Download the compose.yaml +curl https://raw.githubusercontent.com/louislam/dockge/master/compose.yaml --output compose.yaml + +# Start Server +docker compose up -d + +# If you are using docker-compose V1 +# docker-compose up -d +``` + +### Advanced + +If you want to store your stacks in another directory, you can change the `DOCKGE_STACKS_DIR` environment variable and volumes. + +For exmaples, if you want to store your stacks in `/my-stacks`: + +```yaml +version: "3.8" +services: + dockge: + image: louislam/dockge:1 + restart: unless-stopped + ports: + - 5001:5001 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./data:/app/data + + # Your stacks directory in the host + # (The paths inside container must be the same as the host) + - /my-stacks:/my-stacks + environment: + # Tell Dockge where is your stacks directory + - DOCKGE_STACKS_DIR=/my-stacks +``` + +## How to Update + +```bash +cd /opt/stacks +docker compose pull +docker compose up -d +``` + +## Motivations + +- I have been using Portainer for some time, but for the stack management, I am sometimes not satisfied with it. For example, sometimes when I try to deploy a stack, the loading icon keeps spinning for a few minutes without progress. And sometimes error messages are not clear. +- Try to develop with ES Module + TypeScript (Originally, I planned to use Deno or Bun.js, but they do not support for arm64, so I stepped back to Node.js) + +If you love this project, please consider giving this project a ⭐. + + +## FAQ + +#### "Dockge"? + +"Dockge" is a coinage word which is created by myself. I hope it sounds like `Badge` but replacing with `Dock` - `Dock-ge`. + +The naming idea was coming from Twitch emotes like `sadge`, `bedge` or `wokege`. They are all ending with `-ge`. + +If you are not comfortable with the pronunciation, you can call it `Dockage` + +#### Can I manage a single container without `compose.yaml`? + +The main objective of Dockge is that try to use docker `compose.yaml` for everything. If you want to manage a single container, you can just use Portainer or Docker CLI. + +#### Can I manage existing stacks? + +Yes, you can. However, you need to move your compose file into the stacks directory: + +1. Stop your stack +2. Move your compose file into `/opt/stacks//compose.yaml` +3. In Dockge, click the " Scan Stacks Folder" button in the top-right corner's dropdown menu +4. Now you should see your stack in the list + +## More Ideas? + +- Stats +- File manager +- App store for yaml templates +- Get app icons +- Switch Docker context +- Support Dockerfile and build +- Support Docker swarm + + +# Others + +Dockge is built on top of [Compose V2](https://docs.docker.com/compose/migrate/). `compose.yaml` is also known as `docker-compose.yml`. + + diff --git a/backend/check-version.ts b/backend/check-version.ts new file mode 100644 index 0000000..acfb5fb --- /dev/null +++ b/backend/check-version.ts @@ -0,0 +1,71 @@ +import { log } from "./log"; +import compareVersions from "compare-versions"; +import packageJSON from "../package.json"; +import { Settings } from "./settings"; + +export const obj = { + version: packageJSON.version, + latestVersion: null, +}; +export default obj; + +// How much time in ms to wait between update checks +const UPDATE_CHECKER_INTERVAL_MS = 1000 * 60 * 60 * 48; +const CHECK_URL = "https://dockge.kuma.pet/version"; + +let interval : NodeJS.Timeout; + +export function startInterval() { + const check = async () => { + if (await Settings.get("checkUpdate") === false) { + return; + } + + log.debug("update-checker", "Retrieving latest versions"); + + try { + const res = await fetch(CHECK_URL); + const data = await res.json(); + + // For debug + if (process.env.TEST_CHECK_VERSION === "1") { + data.slow = "1000.0.0"; + } + + const checkBeta = await Settings.get("checkBeta"); + + if (checkBeta && data.beta) { + if (compareVersions.compare(data.beta, data.slow, ">")) { + obj.latestVersion = data.beta; + return; + } + } + + if (data.slow) { + obj.latestVersion = data.slow; + } + + } catch (_) { + log.info("update-checker", "Failed to check for new versions"); + } + + }; + + check(); + interval = setInterval(check, UPDATE_CHECKER_INTERVAL_MS); +} + +/** + * Enable the check update feature + * @param value Should the check update feature be enabled? + * @returns + */ +export async function enableCheckUpdate(value : boolean) { + await Settings.set("checkUpdate", value); + + clearInterval(interval); + + if (value) { + startInterval(); + } +} diff --git a/backend/database.ts b/backend/database.ts new file mode 100644 index 0000000..d508af4 --- /dev/null +++ b/backend/database.ts @@ -0,0 +1,247 @@ +import { log } from "./log"; +import { R } from "redbean-node"; +import { DockgeServer } from "./dockge-server"; +import fs from "fs"; +import path from "path"; +import knex from "knex"; + +import Dialect from "knex/lib/dialects/sqlite3/index.js"; + +import sqlite from "@louislam/sqlite3"; +import { sleep } from "./util-common"; + +interface DBConfig { + type?: "sqlite" | "mysql"; +} + +export class Database { + /** + * SQLite file path (Default: ./data/dockge.db) + * @type {string} + */ + static sqlitePath; + + static noReject = true; + + static dbConfig: DBConfig = {}; + + static knexMigrationsPath = "./backend/migrations"; + + private static server : DockgeServer; + + /** + * Use for decode the auth object + */ + jwtSecret? : string; + + static async init(server : DockgeServer) { + this.server = server; + + log.debug("server", "Connecting to the database"); + await Database.connect(); + log.info("server", "Connected to the database"); + + // Patch the database + await Database.patch(); + } + + /** + * Read the database config + * @throws {Error} If the config is invalid + * @typedef {string|undefined} envString + * @returns {{type: "sqlite"} | {type:envString, hostname:envString, port:envString, database:envString, username:envString, password:envString}} Database config + */ + static readDBConfig() { + const dbConfigString = fs.readFileSync(path.join(this.server.config.dataDir, "db-config.json")).toString("utf-8"); + const dbConfig = JSON.parse(dbConfigString); + + if (typeof dbConfig !== "object") { + throw new Error("Invalid db-config.json, it must be an object"); + } + + if (typeof dbConfig.type !== "string") { + throw new Error("Invalid db-config.json, type must be a string"); + } + return dbConfig; + } + + /** + * @typedef {string|undefined} envString + * @param {{type: "sqlite"} | {type:envString, hostname:envString, port:envString, database:envString, username:envString, password:envString}} dbConfig the database configuration that should be written + * @returns {void} + */ + static writeDBConfig(dbConfig) { + fs.writeFileSync(path.join(this.server.config.dataDir, "db-config.json"), JSON.stringify(dbConfig, null, 4)); + } + + /** + * Connect to the database + * @param {boolean} autoloadModels Should models be automatically loaded? + * @param {boolean} noLog Should logs not be output? + * @returns {Promise} + */ + static async connect(autoloadModels = true, noLog = false) { + const acquireConnectionTimeout = 120 * 1000; + let dbConfig; + try { + dbConfig = this.readDBConfig(); + Database.dbConfig = dbConfig; + } catch (err) { + log.warn("db", err.message); + dbConfig = { + type: "sqlite", + }; + this.writeDBConfig(dbConfig); + } + + let config = {}; + + log.info("db", `Database Type: ${dbConfig.type}`); + + if (dbConfig.type === "sqlite") { + this.sqlitePath = path.join(this.server.config.dataDir, "dockge.db"); + Dialect.prototype._driver = () => sqlite; + + config = { + client: Dialect, + connection: { + filename: Database.sqlitePath, + acquireConnectionTimeout: acquireConnectionTimeout, + }, + useNullAsDefault: true, + pool: { + min: 1, + max: 1, + idleTimeoutMillis: 120 * 1000, + propagateCreateError: false, + acquireTimeoutMillis: acquireConnectionTimeout, + } + }; + } else { + throw new Error("Unknown Database type: " + dbConfig.type); + } + + const knexInstance = knex(config); + + // @ts-ignore + R.setup(knexInstance); + + if (process.env.SQL_LOG === "1") { + R.debug(true); + } + + // Auto map the model to a bean object + R.freeze(true); + + if (autoloadModels) { + R.autoloadModels("./backend/models", "ts"); + } + + if (dbConfig.type === "sqlite") { + await this.initSQLite(); + } + } + + /** + @returns {Promise} + */ + static async initSQLite() { + await R.exec("PRAGMA foreign_keys = ON"); + // Change to WAL + await R.exec("PRAGMA journal_mode = WAL"); + await R.exec("PRAGMA cache_size = -12000"); + await R.exec("PRAGMA auto_vacuum = INCREMENTAL"); + + // This ensures that an operating system crash or power failure will not corrupt the database. + // FULL synchronous is very safe, but it is also slower. + // Read more: https://sqlite.org/pragma.html#pragma_synchronous + await R.exec("PRAGMA synchronous = NORMAL"); + + log.debug("db", "SQLite config:"); + log.debug("db", await R.getAll("PRAGMA journal_mode")); + log.debug("db", await R.getAll("PRAGMA cache_size")); + log.debug("db", "SQLite Version: " + await R.getCell("SELECT sqlite_version()")); + } + + /** + * Patch the database + * @returns {void} + */ + static async patch() { + // Using knex migrations + // https://knexjs.org/guide/migrations.html + // https://gist.github.com/NigelEarle/70db130cc040cc2868555b29a0278261 + try { + await R.knex.migrate.latest({ + directory: Database.knexMigrationsPath, + }); + } catch (e) { + // Allow missing patch files for downgrade or testing pr. + if (e.message.includes("the following files are missing:")) { + log.warn("db", e.message); + log.warn("db", "Database migration failed, you may be downgrading Dockge."); + } else { + log.error("db", "Database migration failed"); + throw e; + } + } + } + + /** + * Special handle, because tarn.js throw a promise reject that cannot be caught + * @returns {Promise} + */ + static async close() { + const listener = () => { + Database.noReject = false; + }; + process.addListener("unhandledRejection", listener); + + log.info("db", "Closing the database"); + + // Flush WAL to main database + if (Database.dbConfig.type === "sqlite") { + await R.exec("PRAGMA wal_checkpoint(TRUNCATE)"); + } + + while (true) { + Database.noReject = true; + await R.close(); + await sleep(2000); + + if (Database.noReject) { + break; + } else { + log.info("db", "Waiting to close the database"); + } + } + log.info("db", "Database closed"); + + process.removeListener("unhandledRejection", listener); + } + + /** + * Get the size of the database (SQLite only) + * @returns {number} Size of database + */ + static getSize() { + if (Database.dbConfig.type === "sqlite") { + log.debug("db", "Database.getSize()"); + const stats = fs.statSync(Database.sqlitePath); + log.debug("db", stats); + return stats.size; + } + return 0; + } + + /** + * Shrink the database + * @returns {Promise} + */ + static async shrink() { + if (Database.dbConfig.type === "sqlite") { + await R.exec("VACUUM"); + } + } + +} diff --git a/backend/docker.ts b/backend/docker.ts new file mode 100644 index 0000000..4083a73 --- /dev/null +++ b/backend/docker.ts @@ -0,0 +1,3 @@ +export class Docker { + +} diff --git a/backend/dockge-server.ts b/backend/dockge-server.ts new file mode 100644 index 0000000..ca081b5 --- /dev/null +++ b/backend/dockge-server.ts @@ -0,0 +1,559 @@ +import { MainRouter } from "./routers/main-router"; +import * as fs from "node:fs"; +import { PackageJson } from "type-fest"; +import { Database } from "./database"; +import packageJSON from "../package.json"; +import { log } from "./log"; +import * as socketIO from "socket.io"; +import express, { Express } from "express"; +import { parse } from "ts-command-line-args"; +import https from "https"; +import http from "http"; +import { Router } from "./router"; +import { Socket } from "socket.io"; +import { MainSocketHandler } from "./socket-handlers/main-socket-handler"; +import { SocketHandler } from "./socket-handler"; +import { Settings } from "./settings"; +import checkVersion from "./check-version"; +import dayjs from "dayjs"; +import { R } from "redbean-node"; +import { genSecret, isDev } from "./util-common"; +import { generatePasswordHash } from "./password-hash"; +import { Bean } from "redbean-node/dist/bean"; +import { Arguments, Config, DockgeSocket } from "./util-server"; +import { DockerSocketHandler } from "./socket-handlers/docker-socket-handler"; +import expressStaticGzip from "express-static-gzip"; +import path from "path"; +import { TerminalSocketHandler } from "./socket-handlers/terminal-socket-handler"; +import { Stack } from "./stack"; +import { Cron } from "croner"; +import gracefulShutdown from "http-graceful-shutdown"; +import User from "./models/user"; +import childProcess from "child_process"; + +export class DockgeServer { + app : Express; + httpServer : http.Server; + packageJSON : PackageJson; + io : socketIO.Server; + config : Config; + indexHTML : string = ""; + + /** + * List of express routers + */ + routerList : Router[] = [ + new MainRouter(), + ]; + + /** + * List of socket handlers + */ + socketHandlerList : SocketHandler[] = [ + new MainSocketHandler(), + new DockerSocketHandler(), + new TerminalSocketHandler(), + ]; + + /** + * Show Setup Page + */ + needSetup = false; + + jwtSecret? : string; + + stacksDir : string = ""; + + /** + * + */ + constructor() { + // Catch unexpected errors here + let unexpectedErrorHandler = (error : unknown) => { + console.trace(error); + console.error("If you keep encountering errors, please report to https://github.com/louislam/dockge"); + }; + process.addListener("unhandledRejection", unexpectedErrorHandler); + process.addListener("uncaughtException", unexpectedErrorHandler); + + if (!process.env.NODE_ENV) { + process.env.NODE_ENV = "production"; + } + + // Log NODE ENV + log.info("server", "NODE_ENV: " + process.env.NODE_ENV); + + // Default stacks directory + let defaultStacksDir; + if (process.platform === "win32") { + defaultStacksDir = "./stacks"; + } else { + defaultStacksDir = "/opt/stacks"; + } + + // Define all possible arguments + let args = parse({ + sslKey: { + type: String, + optional: true, + }, + sslCert: { + type: String, + optional: true, + }, + sslKeyPassphrase: { + type: String, + optional: true, + }, + port: { + type: Number, + optional: true, + }, + hostname: { + type: String, + optional: true, + }, + dataDir: { + type: String, + optional: true, + }, + stacksDir: { + type: String, + optional: true, + } + }); + + this.config = args as Config; + + // Load from environment variables or default values if args are not set + this.config.sslKey = args.sslKey || process.env.DOCKGE_SSL_KEY || undefined; + this.config.sslCert = args.sslCert || process.env.DOCKGE_SSL_CERT || undefined; + this.config.sslKeyPassphrase = args.sslKeyPassphrase || process.env.DOCKGE_SSL_KEY_PASSPHRASE || undefined; + this.config.port = args.port || parseInt(process.env.DOCKGE_PORT) || 5001; + this.config.hostname = args.hostname || process.env.DOCKGE_HOSTNAME || undefined; + this.config.dataDir = args.dataDir || process.env.DOCKGE_DATA_DIR || "./data/"; + this.config.stacksDir = args.stacksDir || process.env.DOCKGE_STACKS_DIR || defaultStacksDir; + this.stacksDir = this.config.stacksDir; + + log.debug("server", this.config); + + this.packageJSON = packageJSON as PackageJson; + + try { + this.indexHTML = fs.readFileSync("./frontend-dist/index.html").toString(); + } catch (e) { + // "dist/index.html" is not necessary for development + if (process.env.NODE_ENV !== "development") { + log.error("server", "Error: Cannot find 'frontend-dist/index.html', did you install correctly?"); + process.exit(1); + } + } + + // Create all the necessary directories + this.initDataDir(); + + // Create express + this.app = express(); + + // Create HTTP server + if (this.config.sslKey && this.config.sslCert) { + log.info("server", "Server Type: HTTPS"); + this.httpServer = https.createServer({ + key: fs.readFileSync(this.config.sslKey), + cert: fs.readFileSync(this.config.sslCert), + passphrase: this.config.sslKeyPassphrase, + }, this.app); + } else { + log.info("server", "Server Type: HTTP"); + this.httpServer = http.createServer(this.app); + } + + // Binding Routers + for (const router of this.routerList) { + this.app.use(router.create(this.app, this)); + } + + // Static files + this.app.use("/", expressStaticGzip("frontend-dist", { + enableBrotli: true, + })); + + // Universal Route Handler, must be at the end of all express routes. + this.app.get("*", async (_request, response) => { + response.send(this.indexHTML); + }); + + // Allow all CORS origins in development + let cors = undefined; + if (isDev) { + cors = { + origin: "*", + }; + } + + // Create Socket.io + this.io = new socketIO.Server(this.httpServer, { + cors, + }); + + this.io.on("connection", async (socket: Socket) => { + log.info("server", "Socket connected!"); + + this.sendInfo(socket, true); + + if (this.needSetup) { + log.info("server", "Redirect to setup page"); + socket.emit("setup"); + } + + // Create socket handlers + for (const socketHandler of this.socketHandlerList) { + socketHandler.create(socket as DockgeSocket, this); + } + + // *************************** + // Better do anything after added all socket handlers here + // *************************** + + log.debug("auth", "check auto login"); + if (await Settings.get("disableAuth")) { + log.info("auth", "Disabled Auth: auto login to admin"); + this.afterLogin(socket as DockgeSocket, await R.findOne("user")); + socket.emit("autoLogin"); + } else { + log.debug("auth", "need auth"); + } + + }); + + this.io.on("disconnect", () => { + + }); + + } + + async afterLogin(socket : DockgeSocket, user : User) { + socket.userID = user.id; + socket.join(user.id.toString()); + + this.sendInfo(socket); + + try { + this.sendStackList(); + } catch (e) { + log.error("server", e); + } + } + + /** + * + */ + async serve() { + // Connect to database + try { + await Database.init(this); + } catch (e) { + log.error("server", "Failed to prepare your database: " + e.message); + process.exit(1); + } + + // First time setup if needed + let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [ + "jwtSecret", + ]); + + if (! jwtSecretBean) { + log.info("server", "JWT secret is not found, generate one."); + jwtSecretBean = await this.initJWTSecret(); + log.info("server", "Stored JWT secret into database"); + } else { + log.debug("server", "Load JWT secret from database."); + } + + this.jwtSecret = jwtSecretBean.value; + + const userCount = (await R.knex("user").count("id as count").first()).count; + + log.debug("server", "User count: " + userCount); + + // If there is no record in user table, it is a new Dockge instance, need to setup + if (userCount == 0) { + log.info("server", "No user, need setup"); + this.needSetup = true; + } + + // Listen + this.httpServer.listen(5001, this.config.hostname, () => { + if (this.config.hostname) { + log.info( "server", `Listening on ${this.config.hostname}:${this.config.port}`); + } else { + log.info("server", `Listening on ${this.config.port}`); + } + + // Run every 5 seconds + const job = Cron("*/2 * * * * *", { + protect: true, // Enabled over-run protection. + }, () => { + log.debug("server", "Cron job running"); + this.sendStackList(true); + }); + + }); + + gracefulShutdown(this.httpServer, { + signals: "SIGINT SIGTERM", + timeout: 30000, // timeout: 30 secs + development: false, // not in dev mode + forceExit: true, // triggers process.exit() at the end of shutdown process + onShutdown: this.shutdownFunction, // shutdown function (async) - e.g. for cleanup DB, ... + finally: this.finalFunction, // finally function (sync) - e.g. for logging + }); + + } + + /** + * Emits the version information to the client. + * @param socket Socket.io socket instance + * @param hideVersion Should we hide the version information in the response? + * @returns + */ + async sendInfo(socket : Socket, hideVersion = false) { + let versionProperty; + let latestVersionProperty; + let isContainer; + + if (!hideVersion) { + versionProperty = packageJSON.version; + latestVersionProperty = checkVersion.latestVersion; + isContainer = (process.env.DOCKGE_IS_CONTAINER === "1"); + } + + socket.emit("info", { + version: versionProperty, + latestVersion: latestVersionProperty, + isContainer, + primaryHostname: await Settings.get("primaryHostname"), + //serverTimezone: await this.getTimezone(), + //serverTimezoneOffset: this.getTimezoneOffset(), + }); + } + + /** + * Get the IP of the client connected to the socket + * @param {Socket} socket Socket to query + * @returns IP of client + */ + async getClientIP(socket : Socket) : Promise { + let clientIP = socket.client.conn.remoteAddress; + + if (clientIP === undefined) { + clientIP = ""; + } + + if (await Settings.get("trustProxy")) { + const forwardedFor = socket.client.conn.request.headers["x-forwarded-for"]; + + if (typeof forwardedFor === "string") { + return forwardedFor.split(",")[0].trim(); + } else if (typeof socket.client.conn.request.headers["x-real-ip"] === "string") { + return socket.client.conn.request.headers["x-real-ip"]; + } + } + return clientIP.replace(/^::ffff:/, ""); + } + + /** + * Attempt to get the current server timezone + * If this fails, fall back to environment variables and then make a + * guess. + * @returns {Promise} Current timezone + */ + async getTimezone() { + // From process.env.TZ + try { + if (process.env.TZ) { + this.checkTimezone(process.env.TZ); + return process.env.TZ; + } + } catch (e) { + log.warn("timezone", e.message + " in process.env.TZ"); + } + + const timezone = await Settings.get("serverTimezone"); + + // From Settings + try { + log.debug("timezone", "Using timezone from settings: " + timezone); + if (timezone) { + this.checkTimezone(timezone); + return timezone; + } + } catch (e) { + log.warn("timezone", e.message + " in settings"); + } + + // Guess + try { + const guess = dayjs.tz.guess(); + log.debug("timezone", "Guessing timezone: " + guess); + if (guess) { + this.checkTimezone(guess); + return guess; + } else { + return "UTC"; + } + } catch (e) { + // Guess failed, fall back to UTC + log.debug("timezone", "Guessed an invalid timezone. Use UTC as fallback"); + return "UTC"; + } + } + + /** + * Get the current offset + * @returns {string} Time offset + */ + getTimezoneOffset() { + return dayjs().format("Z"); + } + + /** + * Throw an error if the timezone is invalid + * @param {string} timezone Timezone to test + * @returns {void} + * @throws The timezone is invalid + */ + checkTimezone(timezone : string) { + try { + dayjs.utc("2013-11-18 11:55").tz(timezone).format(); + } catch (e) { + throw new Error("Invalid timezone:" + timezone); + } + } + + /** + * Initialize the data directory + */ + initDataDir() { + if (! fs.existsSync(this.config.dataDir)) { + fs.mkdirSync(this.config.dataDir, { recursive: true }); + } + + // Check if a directory + if (!fs.lstatSync(this.config.dataDir).isDirectory()) { + throw new Error(`Fatal error: ${this.config.dataDir} is not a directory`); + } + + // Create data/stacks directory + if (!fs.existsSync(this.stacksDir)) { + fs.mkdirSync(this.stacksDir, { recursive: true }); + } + + log.info("server", `Data Dir: ${this.config.dataDir}`); + } + + /** + * Init or reset JWT secret + * @returns JWT secret + */ + async initJWTSecret() : Promise { + let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [ + "jwtSecret", + ]); + + if (!jwtSecretBean) { + jwtSecretBean = R.dispense("setting"); + jwtSecretBean.key = "jwtSecret"; + } + + jwtSecretBean.value = generatePasswordHash(genSecret()); + await R.store(jwtSecretBean); + return jwtSecretBean; + } + + sendStackList(useCache = false) { + let roomList = this.io.sockets.adapter.rooms.keys(); + let map : Map | undefined; + + for (let room of roomList) { + // Check if the room is a number (user id) + if (Number(room)) { + + // Get the list only if there is a room + if (!map) { + map = new Map(); + let stackList = Stack.getStackList(this, useCache); + + for (let [ stackName, stack ] of stackList) { + map.set(stackName, stack.toSimpleJSON()); + } + } + + log.debug("server", "Send stack list to room " + room); + this.io.to(room).emit("stackList", { + ok: true, + stackList: Object.fromEntries(map), + }); + } + } + } + + sendStackStatusList() { + let statusList = Stack.getStatusList(); + + let roomList = this.io.sockets.adapter.rooms.keys(); + + for (let room of roomList) { + // Check if the room is a number (user id) + if (Number(room)) { + log.debug("server", "Send stack status list to room " + room); + this.io.to(room).emit("stackStatusList", { + ok: true, + stackStatusList: Object.fromEntries(statusList), + }); + } else { + log.debug("server", "Skip sending stack status list to room " + room); + } + } + } + + getDockerNetworkList() : string[] { + let res = childProcess.spawnSync("docker", [ "network", "ls", "--format", "{{.Name}}" ]); + let list = res.stdout.toString().split("\n"); + + // Remove empty string item + list = list.filter((item) => { + return item !== ""; + }).sort((a, b) => { + return a.localeCompare(b); + }); + + return list; + } + + get stackDirFullPath() { + return path.resolve(this.stacksDir); + } + + /** + * Shutdown the application + * Stops all monitors and closes the database connection. + * @param signal The signal that triggered this function to be called. + */ + async shutdownFunction(signal : string | undefined) { + log.info("server", "Shutdown requested"); + log.info("server", "Called signal: " + signal); + + // TODO: Close all terminals? + + await Database.close(); + Settings.stopCacheCleaner(); + } + + /** + * Final function called before application exits + */ + finalFunction() { + log.info("server", "Graceful shutdown successful!"); + } +} diff --git a/backend/index.ts b/backend/index.ts new file mode 100644 index 0000000..d0b53f2 --- /dev/null +++ b/backend/index.ts @@ -0,0 +1,6 @@ +import { DockgeServer } from "./dockge-server"; +import { log } from "./log"; + +log.info("server", "Welcome to dockge!"); +const server = new DockgeServer(); +await server.serve(); diff --git a/backend/log.ts b/backend/log.ts new file mode 100644 index 0000000..37f2d4a --- /dev/null +++ b/backend/log.ts @@ -0,0 +1,208 @@ +// Console colors +// https://stackoverflow.com/questions/9781218/how-to-change-node-jss-console-font-color +import { intHash, isDev } from "./util-common"; +import dayjs from "dayjs"; + +export const CONSOLE_STYLE_Reset = "\x1b[0m"; +export const CONSOLE_STYLE_Bright = "\x1b[1m"; +export const CONSOLE_STYLE_Dim = "\x1b[2m"; +export const CONSOLE_STYLE_Underscore = "\x1b[4m"; +export const CONSOLE_STYLE_Blink = "\x1b[5m"; +export const CONSOLE_STYLE_Reverse = "\x1b[7m"; +export const CONSOLE_STYLE_Hidden = "\x1b[8m"; + +export const CONSOLE_STYLE_FgBlack = "\x1b[30m"; +export const CONSOLE_STYLE_FgRed = "\x1b[31m"; +export const CONSOLE_STYLE_FgGreen = "\x1b[32m"; +export const CONSOLE_STYLE_FgYellow = "\x1b[33m"; +export const CONSOLE_STYLE_FgBlue = "\x1b[34m"; +export const CONSOLE_STYLE_FgMagenta = "\x1b[35m"; +export const CONSOLE_STYLE_FgCyan = "\x1b[36m"; +export const CONSOLE_STYLE_FgWhite = "\x1b[37m"; +export const CONSOLE_STYLE_FgGray = "\x1b[90m"; +export const CONSOLE_STYLE_FgOrange = "\x1b[38;5;208m"; +export const CONSOLE_STYLE_FgLightGreen = "\x1b[38;5;119m"; +export const CONSOLE_STYLE_FgLightBlue = "\x1b[38;5;117m"; +export const CONSOLE_STYLE_FgViolet = "\x1b[38;5;141m"; +export const CONSOLE_STYLE_FgBrown = "\x1b[38;5;130m"; +export const CONSOLE_STYLE_FgPink = "\x1b[38;5;219m"; + +export const CONSOLE_STYLE_BgBlack = "\x1b[40m"; +export const CONSOLE_STYLE_BgRed = "\x1b[41m"; +export const CONSOLE_STYLE_BgGreen = "\x1b[42m"; +export const CONSOLE_STYLE_BgYellow = "\x1b[43m"; +export const CONSOLE_STYLE_BgBlue = "\x1b[44m"; +export const CONSOLE_STYLE_BgMagenta = "\x1b[45m"; +export const CONSOLE_STYLE_BgCyan = "\x1b[46m"; +export const CONSOLE_STYLE_BgWhite = "\x1b[47m"; +export const CONSOLE_STYLE_BgGray = "\x1b[100m"; + +const consoleModuleColors = [ + CONSOLE_STYLE_FgCyan, + CONSOLE_STYLE_FgGreen, + CONSOLE_STYLE_FgLightGreen, + CONSOLE_STYLE_FgBlue, + CONSOLE_STYLE_FgLightBlue, + CONSOLE_STYLE_FgMagenta, + CONSOLE_STYLE_FgOrange, + CONSOLE_STYLE_FgViolet, + CONSOLE_STYLE_FgBrown, + CONSOLE_STYLE_FgPink, +]; + +const consoleLevelColors : Record = { + "INFO": CONSOLE_STYLE_FgCyan, + "WARN": CONSOLE_STYLE_FgYellow, + "ERROR": CONSOLE_STYLE_FgRed, + "DEBUG": CONSOLE_STYLE_FgGray, +}; + +class Logger { + + /** + * DOCKGE_HIDE_LOG=debug_monitor,info_monitor + * + * Example: + * [ + * "debug_monitor", // Hide all logs that level is debug and the module is monitor + * "info_monitor", + * ] + */ + hideLog : Record = { + info: [], + warn: [], + error: [], + debug: [], + }; + + /** + * + */ + constructor() { + if (typeof process !== "undefined" && process.env.DOCKGE_HIDE_LOG) { + const list = process.env.DOCKGE_HIDE_LOG.split(",").map(v => v.toLowerCase()); + + for (const pair of list) { + // split first "_" only + const values = pair.split(/_(.*)/s); + + if (values.length >= 2) { + this.hideLog[values[0]].push(values[1]); + } + } + + this.debug("server", "DOCKGE_HIDE_LOG is set"); + this.debug("server", this.hideLog); + } + } + + /** + * Write a message to the log + * @param module The module the log comes from + * @param msg Message to write + * @param level Log level. One of INFO, WARN, ERROR, DEBUG or can be customized. + */ + log(module: string, msg: unknown, level: string) { + if (this.hideLog[level] && this.hideLog[level].includes(module.toLowerCase())) { + return; + } + + module = module.toUpperCase(); + level = level.toUpperCase(); + + let now; + if (dayjs.tz) { + now = dayjs.tz(new Date()).format(); + } else { + now = dayjs().format(); + } + + const levelColor = consoleLevelColors[level]; + const moduleColor = consoleModuleColors[intHash(module, consoleModuleColors.length)]; + + let timePart = CONSOLE_STYLE_FgCyan + now + CONSOLE_STYLE_Reset; + const modulePart = "[" + moduleColor + module + CONSOLE_STYLE_Reset + "]"; + const levelPart = levelColor + `${level}:` + CONSOLE_STYLE_Reset; + + if (level === "INFO") { + console.info(timePart, modulePart, levelPart, msg); + } else if (level === "WARN") { + console.warn(timePart, modulePart, levelPart, msg); + } else if (level === "ERROR") { + let msgPart : unknown; + if (typeof msg === "string") { + msgPart = CONSOLE_STYLE_FgRed + msg + CONSOLE_STYLE_Reset; + } else { + msgPart = msg; + } + console.error(timePart, modulePart, levelPart, msgPart); + } else if (level === "DEBUG") { + if (isDev) { + timePart = CONSOLE_STYLE_FgGray + now + CONSOLE_STYLE_Reset; + let msgPart : unknown; + if (typeof msg === "string") { + msgPart = CONSOLE_STYLE_FgGray + msg + CONSOLE_STYLE_Reset; + } else { + msgPart = msg; + } + console.debug(timePart, modulePart, levelPart, msgPart); + } + } else { + console.log(timePart, modulePart, msg); + } + } + + /** + * Log an INFO message + * @param module Module log comes from + * @param msg Message to write + */ + info(module: string, msg: unknown) { + this.log(module, msg, "info"); + } + + /** + * Log a WARN message + * @param module Module log comes from + * @param msg Message to write + */ + warn(module: string, msg: unknown) { + this.log(module, msg, "warn"); + } + + /** + * Log an ERROR message + * @param module Module log comes from + * @param msg Message to write + */ + error(module: string, msg: unknown) { + this.log(module, msg, "error"); + } + + /** + * Log a DEBUG message + * @param module Module log comes from + * @param msg Message to write + */ + debug(module: string, msg: unknown) { + this.log(module, msg, "debug"); + } + + /** + * Log an exception as an ERROR + * @param module Module log comes from + * @param exception The exception to include + * @param msg The message to write + */ + exception(module: string, exception: unknown, msg: unknown) { + let finalMessage = exception; + + if (msg) { + finalMessage = `${msg}: ${exception}`; + } + + this.log(module, finalMessage, "error"); + } +} + +export const log = new Logger(); diff --git a/backend/migrations/2023-10-20-0829-setting-table.ts b/backend/migrations/2023-10-20-0829-setting-table.ts new file mode 100644 index 0000000..56c5d6a --- /dev/null +++ b/backend/migrations/2023-10-20-0829-setting-table.ts @@ -0,0 +1,14 @@ +import { Knex } from "knex"; + +export async function up(knex: Knex): Promise { + return knex.schema.createTable("setting", (table) => { + table.increments("id"); + table.string("key", 200).notNullable().unique().collate("utf8_general_ci"); + table.text("value"); + table.string("type", 20); + }); +} + +export async function down(knex: Knex): Promise { + return knex.schema.dropTable("setting"); +} diff --git a/backend/migrations/2023-10-20-0829-user-table.ts b/backend/migrations/2023-10-20-0829-user-table.ts new file mode 100644 index 0000000..ca84b00 --- /dev/null +++ b/backend/migrations/2023-10-20-0829-user-table.ts @@ -0,0 +1,19 @@ +import { Knex } from "knex"; + +export async function up(knex: Knex): Promise { + // Create the user table + return knex.schema.createTable("user", (table) => { + table.increments("id"); + table.string("username", 255).notNullable().unique().collate("utf8_general_ci"); + table.string("password", 255); + table.boolean("active").notNullable().defaultTo(true); + table.string("timezone", 150); + table.string("twofa_secret", 64); + table.boolean("twofa_status").notNullable().defaultTo(false); + table.string("twofa_last_token", 6); + }); +} + +export async function down(knex: Knex): Promise { + return knex.schema.dropTable("user"); +} diff --git a/backend/models/user.ts b/backend/models/user.ts new file mode 100644 index 0000000..1a91c34 --- /dev/null +++ b/backend/models/user.ts @@ -0,0 +1,46 @@ +import jwt from "jsonwebtoken"; +import { R } from "redbean-node"; +import { BeanModel } from "redbean-node/dist/bean-model"; +import { generatePasswordHash, shake256, SHAKE256_LENGTH } from "../password-hash"; + +export class User extends BeanModel { + /** + * Reset user password + * Fix #1510, as in the context reset-password.js, there is no auto model mapping. Call this static function instead. + * @param {number} userID ID of user to update + * @param {string} newPassword Users new password + * @returns {Promise} + */ + static async resetPassword(userID : number, newPassword : string) { + await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [ + generatePasswordHash(newPassword), + userID + ]); + } + + /** + * Reset this users password + * @param {string} newPassword + * @returns {Promise} + */ + async resetPassword(newPassword : string) { + await User.resetPassword(this.id, newPassword); + this.password = newPassword; + } + + /** + * Create a new JWT for a user + * @param {User} user The User to create a JsonWebToken for + * @param {string} jwtSecret The key used to sign the JsonWebToken + * @returns {string} the JsonWebToken as a string + */ + static createJWT(user : User, jwtSecret : string) { + return jwt.sign({ + username: user.username, + h: shake256(user.password, SHAKE256_LENGTH), + }, jwtSecret); + } + +} + +export default User; diff --git a/backend/password-hash.ts b/backend/password-hash.ts new file mode 100644 index 0000000..fb8d42d --- /dev/null +++ b/backend/password-hash.ts @@ -0,0 +1,47 @@ +import bcrypt from "bcryptjs"; +import crypto from "crypto"; +const saltRounds = 10; + +/** + * Hash a password + * @param {string} password Password to hash + * @returns {string} Hash + */ +export function generatePasswordHash(password : string) { + return bcrypt.hashSync(password, saltRounds); +} + +/** + * Verify a password against a hash + * @param {string} password Password to verify + * @param {string} hash Hash to verify against + * @returns {boolean} Does the password match the hash? + */ +export function verifyPassword(password, hash) { + return bcrypt.compareSync(password, hash); +} + +/** + * Does the hash need to be rehashed? + * @param {string} hash Hash to check + * @returns {boolean} Needs to be rehashed? + */ +export function needRehashPassword(hash : string) : boolean { + return false; +} + +export const SHAKE256_LENGTH = 16; + +/** + * @param {string} data The data to be hashed + * @param {number} len Output length of the hash + * @returns {string} The hashed data in hex format + */ +export function shake256(data, len) { + if (!data) { + return ""; + } + return crypto.createHash("shake256", { outputLength: len }) + .update(data) + .digest("hex"); +} diff --git a/backend/rate-limiter.ts b/backend/rate-limiter.ts new file mode 100644 index 0000000..3c43abe --- /dev/null +++ b/backend/rate-limiter.ts @@ -0,0 +1,75 @@ +// "limit" is bugged in Typescript, use "limiter-es6-compat" instead +// See https://github.com/jhurliman/node-rate-limiter/issues/80 +import { RateLimiter } from "limiter-es6-compat"; +import { log } from "./log"; + +class KumaRateLimiter { + + errorMessage : string; + rateLimiter : RateLimiter; + + /** + * @param {object} config Rate limiter configuration object + */ + constructor(config) { + this.errorMessage = config.errorMessage; + this.rateLimiter = new RateLimiter(config); + } + + /** + * Callback for pass + * @callback passCB + * @param {object} err Too many requests + */ + + /** + * Should the request be passed through + * @param {passCB} callback Callback function to call with decision + * @param {number} num Number of tokens to remove + * @returns {Promise} Should the request be allowed? + */ + async pass(callback, num = 1) { + const remainingRequests = await this.removeTokens(num); + log.info("rate-limit", "remaining requests: " + remainingRequests); + if (remainingRequests < 0) { + if (callback) { + callback({ + ok: false, + msg: this.errorMessage, + }); + } + return false; + } + return true; + } + + /** + * Remove a given number of tokens + * @param {number} num Number of tokens to remove + * @returns {Promise} Number of remaining tokens + */ + async removeTokens(num = 1) { + return await this.rateLimiter.removeTokens(num); + } +} + +export const loginRateLimiter = new KumaRateLimiter({ + tokensPerInterval: 20, + interval: "minute", + fireImmediately: true, + errorMessage: "Too frequently, try again later." +}); + +export const apiRateLimiter = new KumaRateLimiter({ + tokensPerInterval: 60, + interval: "minute", + fireImmediately: true, + errorMessage: "Too frequently, try again later." +}); + +export const twoFaRateLimiter = new KumaRateLimiter({ + tokensPerInterval: 30, + interval: "minute", + fireImmediately: true, + errorMessage: "Too frequently, try again later." +}); diff --git a/backend/router.ts b/backend/router.ts new file mode 100644 index 0000000..b004477 --- /dev/null +++ b/backend/router.ts @@ -0,0 +1,6 @@ +import { DockgeServer } from "./dockge-server"; +import { Express, Router as ExpressRouter } from "express"; + +export abstract class Router { + abstract create(app : Express, server : DockgeServer): ExpressRouter; +} diff --git a/backend/routers/main-router.ts b/backend/routers/main-router.ts new file mode 100644 index 0000000..8d791db --- /dev/null +++ b/backend/routers/main-router.ts @@ -0,0 +1,23 @@ +import { DockgeServer } from "../dockgeServer"; +import { Router } from "../router"; +import express, { Express, Router as ExpressRouter } from "express"; + +export class MainRouter extends Router { + create(app: Express, server: DockgeServer): ExpressRouter { + const router = express.Router(); + + router.get("/", (req, res) => { + res.send(server.indexHTML); + }); + + // Robots.txt + router.get("/robots.txt", async (_request, response) => { + let txt = "User-agent: *\nDisallow: /"; + response.setHeader("Content-Type", "text/plain"); + response.send(txt); + }); + + return router; + } + +} diff --git a/backend/settings.ts b/backend/settings.ts new file mode 100644 index 0000000..fed94a7 --- /dev/null +++ b/backend/settings.ts @@ -0,0 +1,174 @@ +import { R } from "redbean-node"; +import { log } from "./log"; + +export class Settings { + + /** + * Example: + * { + * key1: { + * value: "value2", + * timestamp: 12345678 + * }, + * key2: { + * value: 2, + * timestamp: 12345678 + * }, + * } + * @type {{}} + */ + static cacheList = { + + }; + + static cacheCleaner = null; + + /** + * Retrieve value of setting based on key + * @param {string} key Key of setting to retrieve + * @returns {Promise} Value + */ + static async get(key) { + + // Start cache clear if not started yet + if (!Settings.cacheCleaner) { + Settings.cacheCleaner = setInterval(() => { + log.debug("settings", "Cache Cleaner is just started."); + for (key in Settings.cacheList) { + if (Date.now() - Settings.cacheList[key].timestamp > 60 * 1000) { + log.debug("settings", "Cache Cleaner deleted: " + key); + delete Settings.cacheList[key]; + } + } + + }, 60 * 1000); + } + + // Query from cache + if (key in Settings.cacheList) { + const v = Settings.cacheList[key].value; + log.debug("settings", `Get Setting (cache): ${key}: ${v}`); + return v; + } + + const value = await R.getCell("SELECT `value` FROM setting WHERE `key` = ? ", [ + key, + ]); + + try { + const v = JSON.parse(value); + log.debug("settings", `Get Setting: ${key}: ${v}`); + + Settings.cacheList[key] = { + value: v, + timestamp: Date.now() + }; + + return v; + } catch (e) { + return value; + } + } + + /** + * Sets the specified setting to specified value + * @param {string} key Key of setting to set + * @param {any} value Value to set to + * @param {?string} type Type of setting + * @returns {Promise} + */ + static async set(key, value, type = null) { + + let bean = await R.findOne("setting", " `key` = ? ", [ + key, + ]); + if (!bean) { + bean = R.dispense("setting"); + bean.key = key; + } + bean.type = type; + bean.value = JSON.stringify(value); + await R.store(bean); + + Settings.deleteCache([ key ]); + } + + /** + * Get settings based on type + * @param {string} type The type of setting + * @returns {Promise} Settings + */ + static async getSettings(type) { + const list = await R.getAll("SELECT `key`, `value` FROM setting WHERE `type` = ? ", [ + type, + ]); + + const result = {}; + + for (const row of list) { + try { + result[row.key] = JSON.parse(row.value); + } catch (e) { + result[row.key] = row.value; + } + } + + return result; + } + + /** + * Set settings based on type + * @param {string} type Type of settings to set + * @param {object} data Values of settings + * @returns {Promise} + */ + static async setSettings(type, data) { + const keyList = Object.keys(data); + + const promiseList = []; + + for (const key of keyList) { + let bean = await R.findOne("setting", " `key` = ? ", [ + key + ]); + + if (bean == null) { + bean = R.dispense("setting"); + bean.type = type; + bean.key = key; + } + + if (bean.type === type) { + bean.value = JSON.stringify(data[key]); + promiseList.push(R.store(bean)); + } + } + + await Promise.all(promiseList); + + Settings.deleteCache(keyList); + } + + /** + * Delete selected keys from settings cache + * @param {string[]} keyList Keys to remove + * @returns {void} + */ + static deleteCache(keyList) { + for (const key of keyList) { + delete Settings.cacheList[key]; + } + } + + /** + * Stop the cache cleaner if running + * @returns {void} + */ + static stopCacheCleaner() { + if (Settings.cacheCleaner) { + clearInterval(Settings.cacheCleaner); + Settings.cacheCleaner = null; + } + } +} + diff --git a/backend/socket-handler.ts b/backend/socket-handler.ts new file mode 100644 index 0000000..aaf6060 --- /dev/null +++ b/backend/socket-handler.ts @@ -0,0 +1,6 @@ +import { DockgeServer } from "./dockge-server"; +import { DockgeSocket } from "./util-server"; + +export abstract class SocketHandler { + abstract create(socket : DockgeSocket, server : DockgeServer): void; +} diff --git a/backend/socket-handlers/docker-socket-handler.ts b/backend/socket-handlers/docker-socket-handler.ts new file mode 100644 index 0000000..e945bef --- /dev/null +++ b/backend/socket-handlers/docker-socket-handler.ts @@ -0,0 +1,262 @@ +import { SocketHandler } from "../socket-handler.js"; +import { DockgeServer } from "../dockge-server"; +import { callbackError, checkLogin, DockgeSocket, ValidationError } from "../util-server"; +import { Stack } from "../stack"; + +// @ts-ignore +import composerize from "composerize"; + +export class DockerSocketHandler extends SocketHandler { + create(socket : DockgeSocket, server : DockgeServer) { + + socket.on("deployStack", async (name : unknown, composeYAML : unknown, isAdd : unknown, callback) => { + try { + checkLogin(socket); + const stack = this.saveStack(socket, server, name, composeYAML, isAdd); + await stack.deploy(socket); + server.sendStackList(); + callback({ + ok: true, + msg: "Deployed", + }); + stack.joinCombinedTerminal(socket); + } catch (e) { + callbackError(e, callback); + } + }); + + socket.on("saveStack", async (name : unknown, composeYAML : unknown, isAdd : unknown, callback) => { + try { + checkLogin(socket); + this.saveStack(socket, server, name, composeYAML, isAdd); + callback({ + ok: true, + "msg": "Saved" + }); + server.sendStackList(); + } catch (e) { + callbackError(e, callback); + } + }); + + socket.on("deleteStack", async (name : unknown, callback) => { + try { + checkLogin(socket); + if (typeof(name) !== "string") { + throw new ValidationError("Name must be a string"); + } + const stack = Stack.getStack(server, name); + + try { + await stack.delete(socket); + } catch (e) { + server.sendStackList(); + throw e; + } + + server.sendStackList(); + callback({ + ok: true, + msg: "Deleted" + }); + + } catch (e) { + callbackError(e, callback); + } + }); + + socket.on("getStack", (stackName : unknown, callback) => { + try { + checkLogin(socket); + + if (typeof(stackName) !== "string") { + throw new ValidationError("Stack name must be a string"); + } + + const stack = Stack.getStack(server, stackName); + + stack.joinCombinedTerminal(socket); + + callback({ + ok: true, + stack: stack.toJSON(), + }); + } catch (e) { + callbackError(e, callback); + } + }); + + // requestStackList + socket.on("requestStackList", async (callback) => { + try { + checkLogin(socket); + server.sendStackList(); + callback({ + ok: true, + msg: "Updated" + }); + } catch (e) { + callbackError(e, callback); + } + }); + + // startStack + socket.on("startStack", async (stackName : unknown, callback) => { + try { + checkLogin(socket); + + if (typeof(stackName) !== "string") { + throw new ValidationError("Stack name must be a string"); + } + + const stack = Stack.getStack(server, stackName); + await stack.start(socket); + callback({ + ok: true, + msg: "Started" + }); + server.sendStackList(); + + stack.joinCombinedTerminal(socket); + + } catch (e) { + callbackError(e, callback); + } + }); + + // stopStack + socket.on("stopStack", async (stackName : unknown, callback) => { + try { + checkLogin(socket); + + if (typeof(stackName) !== "string") { + throw new ValidationError("Stack name must be a string"); + } + + const stack = Stack.getStack(server, stackName); + await stack.stop(socket); + callback({ + ok: true, + msg: "Stopped" + }); + server.sendStackList(); + } catch (e) { + callbackError(e, callback); + } + }); + + // restartStack + socket.on("restartStack", async (stackName : unknown, callback) => { + try { + checkLogin(socket); + + if (typeof(stackName) !== "string") { + throw new ValidationError("Stack name must be a string"); + } + + const stack = Stack.getStack(server, stackName); + await stack.restart(socket); + callback({ + ok: true, + msg: "Restarted" + }); + server.sendStackList(); + } catch (e) { + callbackError(e, callback); + } + }); + + // updateStack + socket.on("updateStack", async (stackName : unknown, callback) => { + try { + checkLogin(socket); + + if (typeof(stackName) !== "string") { + throw new ValidationError("Stack name must be a string"); + } + + const stack = Stack.getStack(server, stackName); + await stack.update(socket); + callback({ + ok: true, + msg: "Updated" + }); + server.sendStackList(); + } catch (e) { + callbackError(e, callback); + } + }); + + // Services status + socket.on("serviceStatusList", async (stackName : unknown, callback) => { + try { + checkLogin(socket); + + if (typeof(stackName) !== "string") { + throw new ValidationError("Stack name must be a string"); + } + + const stack = Stack.getStack(server, stackName); + const serviceStatusList = Object.fromEntries(await stack.getServiceStatusList()); + callback({ + ok: true, + serviceStatusList, + }); + } catch (e) { + callbackError(e, callback); + } + }); + + // getExternalNetworkList + socket.on("getDockerNetworkList", async (callback) => { + try { + checkLogin(socket); + const dockerNetworkList = server.getDockerNetworkList(); + callback({ + ok: true, + dockerNetworkList, + }); + } catch (e) { + callbackError(e, callback); + } + }); + + // composerize + socket.on("composerize", async (dockerRunCommand : unknown, callback) => { + try { + checkLogin(socket); + + if (typeof(dockerRunCommand) !== "string") { + throw new ValidationError("dockerRunCommand must be a string"); + } + + const composeTemplate = composerize(dockerRunCommand); + callback({ + ok: true, + composeTemplate, + }); + } catch (e) { + callbackError(e, callback); + } + }); + } + + saveStack(socket : DockgeSocket, server : DockgeServer, name : unknown, composeYAML : unknown, isAdd : unknown) : Stack { + // Check types + if (typeof(name) !== "string") { + throw new ValidationError("Name must be a string"); + } + if (typeof(composeYAML) !== "string") { + throw new ValidationError("Compose YAML must be a string"); + } + if (typeof(isAdd) !== "boolean") { + throw new ValidationError("isAdd must be a boolean"); + } + + const stack = new Stack(server, name, composeYAML); + stack.save(isAdd); + return stack; + } + +} + diff --git a/backend/socket-handlers/main-socket-handler.ts b/backend/socket-handlers/main-socket-handler.ts new file mode 100644 index 0000000..194d093 --- /dev/null +++ b/backend/socket-handlers/main-socket-handler.ts @@ -0,0 +1,295 @@ +import { SocketHandler } from "../socket-handler.js"; +import { Socket } from "socket.io"; +import { DockgeServer } from "../dockge-server"; +import { log } from "../log"; +import { R } from "redbean-node"; +import { loginRateLimiter, twoFaRateLimiter } from "../rate-limiter"; +import { generatePasswordHash, needRehashPassword, shake256, SHAKE256_LENGTH, verifyPassword } from "../password-hash"; +import { User } from "../models/user"; +import { checkLogin, DockgeSocket, doubleCheckPassword } from "../util-server"; +import { passwordStrength } from "check-password-strength"; +import jwt from "jsonwebtoken"; +import { Settings } from "../settings"; + +export class MainSocketHandler extends SocketHandler { + create(socket : DockgeSocket, server : DockgeServer) { + + // *************************** + // Public Socket API + // *************************** + + // Setup + socket.on("setup", async (username, password, callback) => { + try { + if (passwordStrength(password).value === "Too weak") { + throw new Error("Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length."); + } + + if ((await R.knex("user").count("id as count").first()).count !== 0) { + throw new Error("Dockge has been initialized. If you want to run setup again, please delete the database."); + } + + const user = R.dispense("user"); + user.username = username; + user.password = generatePasswordHash(password); + await R.store(user); + + server.needSetup = false; + + callback({ + ok: true, + msg: "successAdded", + msgi18n: true, + }); + + } catch (e) { + callback({ + ok: false, + msg: e.message, + }); + } + }); + + // Login by token + socket.on("loginByToken", async (token, callback) => { + const clientIP = await server.getClientIP(socket); + + log.info("auth", `Login by token. IP=${clientIP}`); + + try { + const decoded = jwt.verify(token, server.jwtSecret); + + log.info("auth", "Username from JWT: " + decoded.username); + + const user = await R.findOne("user", " username = ? AND active = 1 ", [ + decoded.username, + ]) as User; + + if (user) { + // Check if the password changed + if (decoded.h !== shake256(user.password, SHAKE256_LENGTH)) { + throw new Error("The token is invalid due to password change or old token"); + } + + log.debug("auth", "afterLogin"); + await server.afterLogin(socket, user); + log.debug("auth", "afterLogin ok"); + + log.info("auth", `Successfully logged in user ${decoded.username}. IP=${clientIP}`); + + callback({ + ok: true, + }); + } else { + + log.info("auth", `Inactive or deleted user ${decoded.username}. IP=${clientIP}`); + + callback({ + ok: false, + msg: "authUserInactiveOrDeleted", + msgi18n: true, + }); + } + } catch (error) { + log.error("auth", `Invalid token. IP=${clientIP}`); + if (error.message) { + log.error("auth", error.message, `IP=${clientIP}`); + } + callback({ + ok: false, + msg: "authInvalidToken", + msgi18n: true, + }); + } + + }); + + // Login + socket.on("login", async (data, callback) => { + const clientIP = await server.getClientIP(socket); + + log.info("auth", `Login by username + password. IP=${clientIP}`); + + // Checking + if (typeof callback !== "function") { + return; + } + + if (!data) { + return; + } + + // Login Rate Limit + if (!await loginRateLimiter.pass(callback)) { + log.info("auth", `Too many failed requests for user ${data.username}. IP=${clientIP}`); + return; + } + + const user = await this.login(data.username, data.password); + + if (user) { + if (user.twofa_status === 0) { + server.afterLogin(socket, user); + + log.info("auth", `Successfully logged in user ${data.username}. IP=${clientIP}`); + + callback({ + ok: true, + token: User.createJWT(user, server.jwtSecret), + }); + } + + if (user.twofa_status === 1 && !data.token) { + + log.info("auth", `2FA token required for user ${data.username}. IP=${clientIP}`); + + callback({ + tokenRequired: true, + }); + } + + if (data.token) { + const verify = notp.totp.verify(data.token, user.twofa_secret, twoFAVerifyOptions); + + if (user.twofa_last_token !== data.token && verify) { + server.afterLogin(socket, user); + + await R.exec("UPDATE `user` SET twofa_last_token = ? WHERE id = ? ", [ + data.token, + socket.userID, + ]); + + log.info("auth", `Successfully logged in user ${data.username}. IP=${clientIP}`); + + callback({ + ok: true, + token: User.createJWT(user, server.jwtSecret), + }); + } else { + + log.warn("auth", `Invalid token provided for user ${data.username}. IP=${clientIP}`); + + callback({ + ok: false, + msg: "authInvalidToken", + msgi18n: true, + }); + } + } + } else { + + log.warn("auth", `Incorrect username or password for user ${data.username}. IP=${clientIP}`); + + callback({ + ok: false, + msg: "authIncorrectCreds", + msgi18n: true, + }); + } + + }); + + // Change Password + socket.on("changePassword", async (password, callback) => { + try { + checkLogin(socket); + + if (! password.newPassword) { + throw new Error("Invalid new password"); + } + + if (passwordStrength(password.newPassword).value === "Too weak") { + throw new Error("Password is too weak. It should contain alphabetic and numeric characters. It must be at least 6 characters in length."); + } + + let user = await doubleCheckPassword(socket, password.currentPassword); + await user.resetPassword(password.newPassword); + + callback({ + ok: true, + msg: "Password has been updated successfully.", + }); + + } catch (e) { + callback({ + ok: false, + msg: e.message, + }); + } + }); + + socket.on("getSettings", async (callback) => { + try { + checkLogin(socket); + const data = await Settings.getSettings("general"); + + callback({ + ok: true, + data: data, + }); + + } catch (e) { + callback({ + ok: false, + msg: e.message, + }); + } + }); + + socket.on("setSettings", async (data, currentPassword, callback) => { + try { + checkLogin(socket); + + // If currently is disabled auth, don't need to check + // Disabled Auth + Want to Disable Auth => No Check + // Disabled Auth + Want to Enable Auth => No Check + // Enabled Auth + Want to Disable Auth => Check!! + // Enabled Auth + Want to Enable Auth => No Check + const currentDisabledAuth = await Settings.get("disableAuth"); + if (!currentDisabledAuth && data.disableAuth) { + await doubleCheckPassword(socket, currentPassword); + } + + console.log(data); + + await Settings.setSettings("general", data); + + callback({ + ok: true, + msg: "Saved" + }); + + server.sendInfo(socket); + + } catch (e) { + callback({ + ok: false, + msg: e.message, + }); + } + }); + } + + async login(username : string, password : string) { + if (typeof username !== "string" || typeof password !== "string") { + return null; + } + + const user = await R.findOne("user", " username = ? AND active = 1 ", [ + username, + ]); + + if (user && verifyPassword(password, user.password)) { + // Upgrade the hash to bcrypt + if (needRehashPassword(user.password)) { + await R.exec("UPDATE `user` SET password = ? WHERE id = ? ", [ + generatePasswordHash(password), + user.id, + ]); + } + return user; + } + + return null; + } +} diff --git a/backend/socket-handlers/terminal-socket-handler.ts b/backend/socket-handlers/terminal-socket-handler.ts new file mode 100644 index 0000000..f647dfb --- /dev/null +++ b/backend/socket-handlers/terminal-socket-handler.ts @@ -0,0 +1,151 @@ +import { SocketHandler } from "../socket-handler.js"; +import { DockgeServer } from "../dockge-server"; +import { callbackError, checkLogin, DockgeSocket, ValidationError } from "../util-server"; +import { log } from "../log"; +import yaml from "yaml"; +import path from "path"; +import fs from "fs"; +import { + allowedCommandList, + allowedRawKeys, + getComposeTerminalName, getContainerExecTerminalName, + isDev, + PROGRESS_TERMINAL_ROWS +} from "../util-common"; +import { InteractiveTerminal, MainTerminal, Terminal } from "../terminal"; +import { Stack } from "../stack"; + +export class TerminalSocketHandler extends SocketHandler { + create(socket : DockgeSocket, server : DockgeServer) { + + socket.on("terminalInput", async (terminalName : unknown, cmd : unknown, errorCallback) => { + try { + checkLogin(socket); + + if (typeof(terminalName) !== "string") { + throw new Error("Terminal name must be a string."); + } + + if (typeof(cmd) !== "string") { + throw new Error("Command must be a string."); + } + + let terminal = Terminal.getTerminal(terminalName); + if (terminal instanceof InteractiveTerminal) { + //log.debug("terminalInput", "Terminal found, writing to terminal."); + terminal.write(cmd); + } else { + throw new Error("Terminal not found or it is not a Interactive Terminal."); + } + } catch (e) { + errorCallback({ + ok: false, + msg: e.message, + }); + } + }); + + // Main Terminal + socket.on("mainTerminal", async (terminalName : unknown, callback) => { + try { + checkLogin(socket); + + // TODO: Reset the name here, force one main terminal for now + terminalName = "console"; + + if (typeof(terminalName) !== "string") { + throw new ValidationError("Terminal name must be a string."); + } + + log.debug("deployStack", "Terminal name: " + terminalName); + + let terminal = Terminal.getTerminal(terminalName); + + if (!terminal) { + terminal = new MainTerminal(server, terminalName); + terminal.rows = 50; + log.debug("deployStack", "Terminal created"); + } + + terminal.join(socket); + terminal.start(); + + callback({ + ok: true, + }); + } catch (e) { + callbackError(e, callback); + } + }); + + // Interactive Terminal for containers + socket.on("interactiveTerminal", async (stackName : unknown, serviceName : unknown, shell : unknown, callback) => { + try { + checkLogin(socket); + + if (typeof(stackName) !== "string") { + throw new ValidationError("Stack name must be a string."); + } + + if (typeof(serviceName) !== "string") { + throw new ValidationError("Service name must be a string."); + } + + if (typeof(shell) !== "string") { + throw new ValidationError("Shell must be a string."); + } + + log.debug("interactiveTerminal", "Stack name: " + stackName); + log.debug("interactiveTerminal", "Service name: " + serviceName); + + // Get stack + const stack = Stack.getStack(server, stackName); + stack.joinContainerTerminal(socket, serviceName, shell); + + callback({ + ok: true, + }); + } catch (e) { + callbackError(e, callback); + } + }); + + // Join Output Terminal + socket.on("terminalJoin", async (terminalName : unknown, callback) => { + if (typeof(callback) !== "function") { + log.debug("console", "Callback is not a function."); + return; + } + + try { + checkLogin(socket); + if (typeof(terminalName) !== "string") { + throw new ValidationError("Terminal name must be a string."); + } + + let buffer : string = Terminal.getTerminal(terminalName)?.getBuffer() ?? ""; + + if (!buffer) { + log.debug("console", "No buffer found."); + } + + callback({ + ok: true, + buffer, + }); + } catch (e) { + callbackError(e, callback); + } + }); + + // Close Terminal + socket.on("terminalClose", async (terminalName : unknown, callback : unknown) => { + + }); + + // TODO: Resize Terminal + socket.on("terminalResize", async (rows : unknown) => { + + }); + } +} diff --git a/backend/stack.ts b/backend/stack.ts new file mode 100644 index 0000000..5a15fc1 --- /dev/null +++ b/backend/stack.ts @@ -0,0 +1,356 @@ +import { DockgeServer } from "./dockge-server"; +import fs from "fs"; +import { log } from "./log"; +import yaml from "yaml"; +import { DockgeSocket, ValidationError } from "./util-server"; +import path from "path"; +import { + COMBINED_TERMINAL_COLS, + COMBINED_TERMINAL_ROWS, + CREATED_FILE, + CREATED_STACK, + EXITED, getCombinedTerminalName, + getComposeTerminalName, getContainerExecTerminalName, + PROGRESS_TERMINAL_ROWS, + RUNNING, TERMINAL_ROWS, + UNKNOWN +} from "./util-common"; +import { InteractiveTerminal, Terminal } from "./terminal"; +import childProcess from "child_process"; + +export class Stack { + + name: string; + protected _status: number = UNKNOWN; + protected _composeYAML?: string; + protected _configFilePath?: string; + protected server: DockgeServer; + + protected combinedTerminal? : Terminal; + + protected static managedStackList: Map = new Map(); + + constructor(server : DockgeServer, name : string, composeYAML? : string) { + this.name = name; + this.server = server; + this._composeYAML = composeYAML; + } + + toJSON() : object { + let obj = this.toSimpleJSON(); + return { + ...obj, + composeYAML: this.composeYAML, + }; + } + + toSimpleJSON() : object { + return { + name: this.name, + status: this._status, + tags: [], + isManagedByDockge: this.isManagedByDockge, + }; + } + + /** + * Get the status of the stack from `docker compose ps --format json` + */ + ps() : object { + let res = childProcess.execSync("docker compose ps --format json", { + cwd: this.path + }); + return JSON.parse(res.toString()); + } + + get isManagedByDockge() : boolean { + return fs.existsSync(this.path) && fs.statSync(this.path).isDirectory(); + } + + get status() : number { + return this._status; + } + + validate() { + // Check name, allows [a-z][A-Z][0-9] _ - only + if (!this.name.match(/^[a-zA-Z0-9_-]+$/)) { + throw new ValidationError("Stack name can only contain [a-z][A-Z][0-9] _ - only"); + } + + // Check YAML format + yaml.parse(this.composeYAML); + } + + get composeYAML() : string { + if (this._composeYAML === undefined) { + try { + this._composeYAML = fs.readFileSync(path.join(this.path, "compose.yaml"), "utf-8"); + } catch (e) { + this._composeYAML = ""; + } + } + return this._composeYAML; + } + + get path() : string { + return path.join(this.server.stacksDir, this.name); + } + + get fullPath() : string { + let dir = this.path; + + // Compose up via node-pty + let fullPathDir; + + // if dir is relative, make it absolute + if (!path.isAbsolute(dir)) { + fullPathDir = path.join(process.cwd(), dir); + } else { + fullPathDir = dir; + } + return fullPathDir; + } + + /** + * Save the stack to the disk + * @param isAdd + */ + save(isAdd : boolean) { + this.validate(); + + let dir = this.path; + + // Check if the name is used if isAdd + if (isAdd) { + if (fs.existsSync(dir)) { + throw new ValidationError("Stack name already exists"); + } + + // Create the stack folder + fs.mkdirSync(dir); + } else { + if (!fs.existsSync(dir)) { + throw new ValidationError("Stack not found"); + } + } + + // Write or overwrite the compose.yaml + fs.writeFileSync(path.join(dir, "compose.yaml"), this.composeYAML); + } + + async deploy(socket? : DockgeSocket) : Promise { + const terminalName = getComposeTerminalName(this.name); + let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "up", "-d", "--remove-orphans" ], this.path); + if (exitCode !== 0) { + throw new Error("Failed to deploy, please check the terminal output for more information."); + } + return exitCode; + } + + async delete(socket?: DockgeSocket) : Promise { + const terminalName = getComposeTerminalName(this.name); + let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "down", "--remove-orphans", "--rmi", "all" ], this.path); + if (exitCode !== 0) { + throw new Error("Failed to delete, please check the terminal output for more information."); + } + + // Remove the stack folder + fs.rmSync(this.path, { + recursive: true, + force: true + }); + + return exitCode; + } + + static getStackList(server : DockgeServer, useCacheForManaged = false) : Map { + let stacksDir = server.stacksDir; + let stackList : Map; + + if (useCacheForManaged && this.managedStackList.size > 0) { + stackList = this.managedStackList; + } else { + stackList = new Map(); + + // Scan the stacks directory, and get the stack list + let filenameList = fs.readdirSync(stacksDir); + + for (let filename of filenameList) { + try { + let stack = this.getStack(server, filename); + stack._status = CREATED_FILE; + stackList.set(filename, stack); + } catch (e) { + log.warn("getStackList", `Failed to get stack ${filename}, error: ${e.message}`); + } + } + + // Cache by copying + this.managedStackList = new Map(stackList); + } + + // Also get the list from `docker compose ls --all --format json` + let res = childProcess.execSync("docker compose ls --all --format json"); + let composeList = JSON.parse(res.toString()); + + for (let composeStack of composeList) { + + // Skip the dockge stack + // TODO: Could be self managed? + if (composeStack.Name === "dockge") { + continue; + } + + let stack = stackList.get(composeStack.Name); + + // This stack probably is not managed by Dockge, but we still want to show it + if (!stack) { + stack = new Stack(server, composeStack.Name); + stackList.set(composeStack.Name, stack); + } + + stack._status = this.statusConvert(composeStack.Status); + stack._configFilePath = composeStack.ConfigFiles; + } + + return stackList; + } + + /** + * Get the status list, it will be used to update the status of the stacks + * Not all status will be returned, only the stack that is deployed or created to `docker compose` will be returned + */ + static getStatusList() : Map { + let statusList = new Map(); + + let res = childProcess.execSync("docker compose ls --all --format json"); + let composeList = JSON.parse(res.toString()); + + for (let composeStack of composeList) { + statusList.set(composeStack.Name, this.statusConvert(composeStack.Status)); + } + + return statusList; + } + + /** + * Convert the status string from `docker compose ls` to the status number + * @param status + */ + static statusConvert(status : string) : number { + if (status.startsWith("created")) { + return CREATED_STACK; + } else if (status.startsWith("running")) { + return RUNNING; + } else if (status.startsWith("exited")) { + return EXITED; + } else { + return UNKNOWN; + } + } + + static getStack(server: DockgeServer, stackName: string) : Stack { + let dir = path.join(server.stacksDir, stackName); + + if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) { + // Maybe it is a stack managed by docker compose directly + let stackList = this.getStackList(server); + let stack = stackList.get(stackName); + + if (stack) { + return stack; + } else { + // Really not found + throw new ValidationError("Stack not found"); + } + } + + let stack = new Stack(server, stackName); + stack._status = UNKNOWN; + stack._configFilePath = path.resolve(dir); + return stack; + } + + async start(socket: DockgeSocket) { + const terminalName = getComposeTerminalName(this.name); + let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "up", "-d", "--remove-orphans" ], this.path); + if (exitCode !== 0) { + throw new Error("Failed to start, please check the terminal output for more information."); + } + return exitCode; + } + + async stop(socket: DockgeSocket) : Promise { + const terminalName = getComposeTerminalName(this.name); + let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "stop" ], this.path); + if (exitCode !== 0) { + throw new Error("Failed to stop, please check the terminal output for more information."); + } + return exitCode; + } + + async restart(socket: DockgeSocket) : Promise { + const terminalName = getComposeTerminalName(this.name); + let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "restart" ], this.path); + if (exitCode !== 0) { + throw new Error("Failed to restart, please check the terminal output for more information."); + } + return exitCode; + } + + async update(socket: DockgeSocket) { + const terminalName = getComposeTerminalName(this.name); + let exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "pull" ], this.path); + if (exitCode !== 0) { + throw new Error("Failed to pull, please check the terminal output for more information."); + } + exitCode = await Terminal.exec(this.server, socket, terminalName, "docker", [ "compose", "up", "-d", "--remove-orphans" ], this.path); + if (exitCode !== 0) { + throw new Error("Failed to restart, please check the terminal output for more information."); + } + return exitCode; + } + + async joinCombinedTerminal(socket: DockgeSocket) { + const terminalName = getCombinedTerminalName(this.name); + const terminal = Terminal.getOrCreateTerminal(this.server, terminalName, "docker", [ "compose", "logs", "-f", "--tail", "100" ], this.path); + terminal.rows = COMBINED_TERMINAL_ROWS; + terminal.cols = COMBINED_TERMINAL_COLS; + terminal.join(socket); + terminal.start(); + } + + async joinContainerTerminal(socket: DockgeSocket, serviceName: string, shell : string = "sh", index: number = 0) { + const terminalName = getContainerExecTerminalName(this.name, serviceName, index); + let terminal = Terminal.getTerminal(terminalName); + + if (!terminal) { + terminal = new InteractiveTerminal(this.server, terminalName, "docker", [ "compose", "exec", serviceName, shell ], this.path); + terminal.rows = TERMINAL_ROWS; + log.debug("joinContainerTerminal", "Terminal created"); + } + + terminal.join(socket); + terminal.start(); + } + + async getServiceStatusList() { + let statusList = new Map(); + + let res = childProcess.execSync("docker compose ps --format json", { + cwd: this.path, + }); + + let lines = res.toString().split("\n"); + + for (let line of lines) { + try { + let obj = JSON.parse(line); + statusList.set(obj.Service, obj.State); + } catch (e) { + } + } + + return statusList; + } +} diff --git a/backend/terminal.ts b/backend/terminal.ts new file mode 100644 index 0000000..2a31f17 --- /dev/null +++ b/backend/terminal.ts @@ -0,0 +1,230 @@ +import { DockgeServer } from "./dockge-server"; +import * as os from "node:os"; +import * as pty from "@homebridge/node-pty-prebuilt-multiarch"; +import { LimitQueue } from "./utils/limit-queue"; +import { DockgeSocket } from "./util-server"; +import { + allowedCommandList, allowedRawKeys, + getComposeTerminalName, + getCryptoRandomInt, + PROGRESS_TERMINAL_ROWS, + TERMINAL_COLS, + TERMINAL_ROWS +} from "./util-common"; +import { sync as commandExistsSync } from "command-exists"; +import { log } from "./log"; + +/** + * Terminal for running commands, no user interaction + */ +export class Terminal { + + protected static terminalMap : Map = new Map(); + + protected _ptyProcess? : pty.IPty; + protected server : DockgeServer; + protected buffer : LimitQueue = new LimitQueue(100); + protected _name : string; + + protected file : string; + protected args : string | string[]; + protected cwd : string; + protected callback? : (exitCode : number) => void; + + protected _rows : number = TERMINAL_ROWS; + protected _cols : number = TERMINAL_COLS; + + constructor(server : DockgeServer, name : string, file : string, args : string | string[], cwd : string) { + this.server = server; + this._name = name; + //this._name = "terminal-" + Date.now() + "-" + getCryptoRandomInt(0, 1000000); + this.file = file; + this.args = args; + this.cwd = cwd; + + Terminal.terminalMap.set(this.name, this); + } + + get rows() { + return this._rows; + } + + set rows(rows : number) { + this._rows = rows; + try { + this.ptyProcess?.resize(this.cols, this.rows); + } catch (e) { + log.debug("Terminal", "Failed to resize terminal: " + e.message); + } + } + + get cols() { + return this._cols; + } + + set cols(cols : number) { + this._cols = cols; + try { + this.ptyProcess?.resize(this.cols, this.rows); + } catch (e) { + log.debug("Terminal", "Failed to resize terminal: " + e.message); + } + } + + public start() { + if (this._ptyProcess) { + return; + } + + this._ptyProcess = pty.spawn(this.file, this.args, { + name: this.name, + cwd: this.cwd, + cols: TERMINAL_COLS, + rows: this.rows, + }); + + // On Data + this._ptyProcess.onData((data) => { + this.buffer.push(data); + if (this.server.io) { + this.server.io.to(this.name).emit("terminalWrite", this.name, data); + } + }); + + // On Exit + this._ptyProcess.onExit((res) => { + this.server.io.to(this.name).emit("terminalExit", this.name, res.exitCode); + + // Remove room + this.server.io.in(this.name).socketsLeave(this.name); + + Terminal.terminalMap.delete(this.name); + log.debug("Terminal", "Terminal " + this.name + " exited with code " + res.exitCode); + + if (this.callback) { + this.callback(res.exitCode); + } + }); + } + + public onExit(callback : (exitCode : number) => void) { + this.callback = callback; + } + + public join(socket : DockgeSocket) { + socket.join(this.name); + } + + public leave(socket : DockgeSocket) { + socket.leave(this.name); + } + + public get ptyProcess() { + return this._ptyProcess; + } + + public get name() { + return this._name; + } + + /** + * Get the terminal output string + */ + getBuffer() : string { + if (this.buffer.length === 0) { + return ""; + } + return this.buffer.join(""); + } + + close() { + this._ptyProcess?.kill(); + } + + /** + * Get a running and non-exited terminal + * @param name + */ + public static getTerminal(name : string) : Terminal | undefined { + return Terminal.terminalMap.get(name); + } + + public static getOrCreateTerminal(server : DockgeServer, name : string, file : string, args : string | string[], cwd : string) : Terminal { + // Since exited terminal will be removed from the map, it is safe to get the terminal from the map + let terminal = Terminal.getTerminal(name); + if (!terminal) { + terminal = new Terminal(server, name, file, args, cwd); + } + return terminal; + } + + public static exec(server : DockgeServer, socket : DockgeSocket | undefined, terminalName : string, file : string, args : string | string[], cwd : string) : Promise { + const terminal = new Terminal(server, terminalName, file, args, cwd); + terminal.rows = PROGRESS_TERMINAL_ROWS; + + if (socket) { + terminal.join(socket); + } + + return new Promise((resolve) => { + terminal.onExit((exitCode : number) => { + resolve(exitCode); + }); + terminal.start(); + }); + } +} + +/** + * Interactive terminal + * Mainly used for container exec + */ +export class InteractiveTerminal extends Terminal { + public write(input : string) { + this.ptyProcess?.write(input); + } + + resetCWD() { + const cwd = process.cwd(); + this.ptyProcess?.write(`cd "${cwd}"\r`); + } +} + +/** + * User interactive terminal that use bash or powershell with limited commands such as docker, ls, cd, dir + */ +export class MainTerminal extends InteractiveTerminal { + constructor(server : DockgeServer, name : string) { + let shell; + + if (os.platform() === "win32") { + if (commandExistsSync("pwsh.exe")) { + shell = "pwsh.exe"; + } else { + shell = "powershell.exe"; + } + } else { + shell = "bash"; + } + super(server, name, shell, [], server.stacksDir); + } + + public write(input : string) { + // For like Ctrl + C + if (allowedRawKeys.includes(input)) { + super.write(input); + return; + } + + // Check if the command is allowed + const cmdParts = input.split(" "); + const executable = cmdParts[0].trim(); + log.debug("console", "Executable: " + executable); + log.debug("console", "Executable length: " + executable.length); + + if (!allowedCommandList.includes(executable)) { + throw new Error("Command not allowed."); + } + super.write(input); + } +} diff --git a/backend/util-common.ts b/backend/util-common.ts new file mode 100644 index 0000000..576d8d8 --- /dev/null +++ b/backend/util-common.ts @@ -0,0 +1,337 @@ +/* + * Common utilities for backend and frontend + */ +import { Document } from "yaml"; + +// Init dayjs +import dayjs from "dayjs"; +import timezone from "dayjs/plugin/timezone"; +import utc from "dayjs/plugin/utc"; +import relativeTime from "dayjs/plugin/relativeTime"; +dayjs.extend(utc); +dayjs.extend(timezone); +dayjs.extend(relativeTime); + +let randomBytes : (numBytes: number) => Uint8Array; +initRandomBytes(); + +async function initRandomBytes() { + if (typeof window !== "undefined" && window.crypto) { + randomBytes = function randomBytes(numBytes: number) { + const bytes = new Uint8Array(numBytes); + for (let i = 0; i < numBytes; i += 65536) { + window.crypto.getRandomValues(bytes.subarray(i, i + Math.min(numBytes - i, 65536))); + } + return bytes; + }; + } else { + randomBytes = (await import("node:crypto")).randomBytes; + } +} + +// Stack Status +export const UNKNOWN = 0; +export const CREATED_FILE = 1; +export const CREATED_STACK = 2; +export const RUNNING = 3; +export const EXITED = 4; + +export function statusName(status : number) : string { + switch (status) { + case CREATED_FILE: + return "draft"; + case CREATED_STACK: + return "created_stack"; + case RUNNING: + return "running"; + case EXITED: + return "exited"; + default: + return "unknown"; + } +} + +export function statusNameShort(status : number) : string { + switch (status) { + case CREATED_FILE: + return "inactive"; + case CREATED_STACK: + return "inactive"; + case RUNNING: + return "active"; + case EXITED: + return "exited"; + default: + return "?"; + } +} + +export function statusColor(status : number) : string { + switch (status) { + case CREATED_FILE: + return "dark"; + case CREATED_STACK: + return "dark"; + case RUNNING: + return "primary"; + case EXITED: + return "danger"; + default: + return "secondary"; + } +} + +export const isDev = process.env.NODE_ENV === "development"; +export const TERMINAL_COLS = 105; +export const TERMINAL_ROWS = 10; +export const PROGRESS_TERMINAL_ROWS = 8; + +export const COMBINED_TERMINAL_COLS = 56; +export const COMBINED_TERMINAL_ROWS = 15; + +export const ERROR_TYPE_VALIDATION = 1; + +export const allowedCommandList : string[] = [ + "docker", + "ls", + "cd", + "dir", +]; + +export const allowedRawKeys = [ + "\u0003", // Ctrl + C +]; + +/** + * Generate a decimal integer number from a string + * @param str Input + * @param length Default is 10 which means 0 - 9 + */ +export function intHash(str : string, length = 10) : number { + // A simple hashing function (you can use more complex hash functions if needed) + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash += str.charCodeAt(i); + } + // Normalize the hash to the range [0, 10] + return (hash % length + length) % length; // Ensure the result is non-negative +} + +/** + * Delays for specified number of seconds + * @param ms Number of milliseconds to sleep for + */ +export function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +/** + * Generate a random alphanumeric string of fixed length + * @param length Length of string to generate + * @returns string + */ +export function genSecret(length = 64) { + let secret = ""; + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + const charsLength = chars.length; + for ( let i = 0; i < length; i++ ) { + secret += chars.charAt(getCryptoRandomInt(0, charsLength - 1)); + } + return secret; +} + +/** + * Get a random integer suitable for use in cryptography between upper + * and lower bounds. + * @param min Minimum value of integer + * @param max Maximum value of integer + * @returns Cryptographically suitable random integer + */ +export function getCryptoRandomInt(min: number, max: number):number { + // synchronous version of: https://github.com/joepie91/node-random-number-csprng + + const range = max - min; + if (range >= Math.pow(2, 32)) { + console.log("Warning! Range is too large."); + } + + let tmpRange = range; + let bitsNeeded = 0; + let bytesNeeded = 0; + let mask = 1; + + while (tmpRange > 0) { + if (bitsNeeded % 8 === 0) { + bytesNeeded += 1; + } + bitsNeeded += 1; + mask = mask << 1 | 1; + tmpRange = tmpRange >>> 1; + } + + const bytes = randomBytes(bytesNeeded); + let randomValue = 0; + + for (let i = 0; i < bytesNeeded; i++) { + randomValue |= bytes[i] << 8 * i; + } + + randomValue = randomValue & mask; + + if (randomValue <= range) { + return min + randomValue; + } else { + return getCryptoRandomInt(min, max); + } +} + +export function getComposeTerminalName(stack : string) { + return "compose-" + stack; +} + +export function getCombinedTerminalName(stack : string) { + return "combined-" + stack; +} + +export function getContainerTerminalName(container : string) { + return "container-" + container; +} + +export function getContainerExecTerminalName(stackName : string, container : string, index : number) { + return "container-exec-" + container + "-" + index; +} + +export function copyYAMLComments(doc : Document, src : Document) { + doc.comment = src.comment; + doc.commentBefore = src.commentBefore; + + if (doc && doc.contents && src && src.contents) { + // @ts-ignore + copyYAMLCommentsItems(doc.contents.items, src.contents.items); + } +} + +/** + * Copy yaml comments from srcItems to items + * Typescript is super annoying here, so I have to use any here + * TODO: Since comments are belong to the array index, the comments will be lost if the order of the items is changed or removed or added. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function copyYAMLCommentsItems(items : any, srcItems : any) { + if (!items || !srcItems) { + return; + } + + for (let i = 0; i < items.length; i++) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const item : any = items[i]; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const srcItem : any = srcItems[i]; + + if (!srcItem) { + continue; + } + + if (item.key && srcItem.key) { + item.key.comment = srcItem.key.comment; + item.key.commentBefore = srcItem.key.commentBefore; + } + + if (srcItem.comment) { + item.comment = srcItem.comment; + } + + if (item.value && srcItem.value) { + if (typeof item.value === "object" && typeof srcItem.value === "object") { + item.value.comment = srcItem.value.comment; + item.value.commentBefore = srcItem.value.commentBefore; + + if (item.value.items && srcItem.value.items) { + copyYAMLCommentsItems(item.value.items, srcItem.value.items); + } + } + } + } +} + +/** + * Possible Inputs: + * ports: + * - "3000" + * - "3000-3005" + * - "8000:8000" + * - "9090-9091:8080-8081" + * - "49100:22" + * - "8000-9000:80" + * - "127.0.0.1:8001:8001" + * - "127.0.0.1:5000-5010:5000-5010" + * - "6060:6060/udp" + * @param input + * @param defaultHostname + */ +export function parseDockerPort(input : string, defaultHostname : string = "localhost") { + let hostname = defaultHostname; + let port; + let display; + + const parts = input.split("/"); + const part1 = parts[0]; + let protocol = parts[1] || "tcp"; + + // Split the last ":" + const lastColon = part1.lastIndexOf(":"); + + if (lastColon === -1) { + // No colon, so it's just a port or port range + // Check if it's a port range + const dash = part1.indexOf("-"); + if (dash === -1) { + // No dash, so it's just a port + port = part1; + } else { + // Has dash, so it's a port range, use the first port + port = part1.substring(0, dash); + } + + display = part1; + + } else { + // Has colon, so it's a port mapping + let hostPart = part1.substring(0, lastColon); + display = hostPart; + + // Check if it's a port range + const dash = part1.indexOf("-"); + + if (dash !== -1) { + // Has dash, so it's a port range, use the first port + hostPart = part1.substring(0, dash); + } + + // Check if it has a ip (ip:port) + const colon = hostPart.indexOf(":"); + + if (colon !== -1) { + // Has colon, so it's a ip:port + hostname = hostPart.substring(0, colon); + port = hostPart.substring(colon + 1); + } else { + // No colon, so it's just a port + port = hostPart; + } + } + + let portInt = parseInt(port); + + if (portInt == 443) { + protocol = "https"; + } else if (protocol === "tcp") { + protocol = "http"; + } + + return { + url: protocol + "://" + hostname + ":" + portInt, + display: display, + }; +} diff --git a/backend/util-server.ts b/backend/util-server.ts new file mode 100644 index 0000000..241782c --- /dev/null +++ b/backend/util-server.ts @@ -0,0 +1,79 @@ +import { Socket } from "socket.io"; +import { Terminal } from "./terminal"; +import { randomBytes } from "crypto"; +import { log } from "./log"; +import { ERROR_TYPE_VALIDATION } from "./util-common"; +import { R } from "redbean-node"; +import { verifyPassword } from "./password-hash"; + +export interface DockgeSocket extends Socket { + userID: number; + consoleTerminal? : Terminal; +} + +// For command line arguments, so they are nullable +export interface Arguments { + sslKey? : string; + sslCert? : string; + sslKeyPassphrase? : string; + port? : number; + hostname? : string; + dataDir? : string; + stacksDir? : string; +} + +// Some config values are required +export interface Config extends Arguments { + dataDir : string; + stacksDir : string; +} + +export function checkLogin(socket : DockgeSocket) { + if (!socket.userID) { + throw new Error("You are not logged in."); + } +} + +export class ValidationError extends Error { + constructor(message : string) { + super(message); + } +} + +export function callbackError(error : unknown, callback : unknown) { + if (typeof(callback) !== "function") { + log.error("console", "Callback is not a function"); + return; + } + + if (error instanceof Error) { + callback({ + ok: false, + msg: error.message, + }); + } else if (error instanceof ValidationError) { + callback({ + ok: false, + type: ERROR_TYPE_VALIDATION, + msg: error.message, + }); + } else { + log.debug("console", "Unknown error: " + error); + } +} + +export async function doubleCheckPassword(socket : DockgeSocket, currentPassword : unknown) { + if (typeof currentPassword !== "string") { + throw new Error("Wrong data type?"); + } + + let user = await R.findOne("user", " id = ? AND active = 1 ", [ + socket.userID, + ]); + + if (!user || !verifyPassword(currentPassword, user.password)) { + throw new Error("Incorrect current password"); + } + + return user; +} diff --git a/backend/utils/limit-queue.ts b/backend/utils/limit-queue.ts new file mode 100644 index 0000000..8a1a95d --- /dev/null +++ b/backend/utils/limit-queue.ts @@ -0,0 +1,24 @@ +/** + * Limit Queue + * The first element will be removed when the length exceeds the limit + */ +export class LimitQueue extends Array { + __limit; + __onExceed = null; + + constructor(limit: number) { + super(); + this.__limit = limit; + } + + push(value : T) { + super.push(value); + if (this.length > this.__limit) { + const item = this.shift(); + if (this.__onExceed) { + this.__onExceed(item); + } + } + } + +} diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..018a6cb --- /dev/null +++ b/compose.yaml @@ -0,0 +1,18 @@ +version: "3.8" +services: + dockge: + image: louislam/dockge:1 + restart: unless-stopped + ports: + # Host Port : Container Port + - 5001:5001 + volumes: + # Docker Socket + - /var/run/docker.sock:/var/run/docker.sock + # Dockge Config + - ./data:/app/data + # Your stacks directory in the host (The paths inside container must be the same as the host) + - /opt/stacks:/opt/stacks + environment: + # Tell Dockge where is your stacks directory + - DOCKGE_STACKS_DIR=/opt/stacks diff --git a/docker/Base.Dockerfile b/docker/Base.Dockerfile new file mode 100644 index 0000000..7c89d95 --- /dev/null +++ b/docker/Base.Dockerfile @@ -0,0 +1,39 @@ +FROM node:20-bookworm-slim +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" + +# COPY --from=docker:dind /usr/local/bin/docker /usr/local/bin/ + +RUN apt update && apt install --yes --no-install-recommends \ + curl \ + ca-certificates \ + gnupg \ + unzip \ + dumb-init \ + && install -m 0755 -d /etc/apt/keyrings \ + && curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \ + && chmod a+r /etc/apt/keyrings/docker.gpg \ + && echo \ + "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \ + "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \ + tee /etc/apt/sources.list.d/docker.list > /dev/null \ + && apt update \ + && apt --yes --no-install-recommends install \ + docker-ce-cli \ + docker-compose-plugin \ + && rm -rf /var/lib/apt/lists/* \ + && npm install pnpm -g \ + && pnpm install -g tsx + +# ensures that /var/run/docker.sock exists +# changes the ownership of /var/run/docker.sock +RUN touch /var/run/docker.sock && chown node:node /var/run/docker.sock + +# Full Base Image +# MariaDB, Chromium and fonts +#FROM base-slim AS base +#ENV DOCKGE_ENABLE_EMBEDDED_MARIADB=1 +#RUN apt update && \ +# apt --yes --no-install-recommends install mariadb-server && \ +# rm -rf /var/lib/apt/lists/* && \ +# apt --yes autoremove diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..fa70174 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,29 @@ +############################################ +# Build +############################################ +FROM louislam/dockge:base AS build +WORKDIR /app +COPY --chown=node:node ./package.json ./package.json +COPY --chown=node:node ./pnpm-lock.yaml ./pnpm-lock.yaml +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile + +############################################ +# ⭐ Main Image +############################################ +FROM louislam/dockge:base AS release +WORKDIR /app +COPY --chown=node:node . . +COPY --from=build /app/node_modules /app/node_modules +RUN mkdir ./data + +VOLUME /app/data +EXPOSE 5001 +ENTRYPOINT ["/usr/bin/dumb-init", "--"] +CMD ["tsx", "./backend/index.ts"] + + +############################################ +# Mark as Nightly +############################################ +FROM release AS nightly +RUN pnpm run mark-as-nightly diff --git a/extra/mark-as-nightly.ts b/extra/mark-as-nightly.ts new file mode 100644 index 0000000..afb863f --- /dev/null +++ b/extra/mark-as-nightly.ts @@ -0,0 +1,22 @@ +import pkg from "../package.json"; +import fs from "fs"; +import dayjs from "dayjs"; + +const oldVersion = pkg.version; +const newVersion = oldVersion + "-nightly-" + dayjs().format("YYYYMMDDHHmmss"); + +console.log("Old Version: " + oldVersion); +console.log("New Version: " + newVersion); + +if (newVersion) { + // Process package.json + pkg.version = newVersion; + //pkg.scripts.setup = pkg.scripts.setup.replaceAll(oldVersion, newVersion); + //pkg.scripts["build-docker"] = pkg.scripts["build-docker"].replaceAll(oldVersion, newVersion); + fs.writeFileSync("package.json", JSON.stringify(pkg, null, 4) + "\n"); + + // Process README.md + if (fs.existsSync("README.md")) { + fs.writeFileSync("README.md", fs.readFileSync("README.md", "utf8").replaceAll(oldVersion, newVersion)); + } +} diff --git a/extra/templates/mariadb/compose.yaml b/extra/templates/mariadb/compose.yaml new file mode 100644 index 0000000..14aad83 --- /dev/null +++ b/extra/templates/mariadb/compose.yaml @@ -0,0 +1,9 @@ +version: "3.8" +services: + mariadb: + image: mariadb:latest + restart: unless-stopped + ports: + - 3306:3306 + environment: + - MARIADB_ROOT_PASSWORD=123456 diff --git a/extra/templates/nginx-proxy-manager/compose.yaml b/extra/templates/nginx-proxy-manager/compose.yaml new file mode 100644 index 0000000..49d5813 --- /dev/null +++ b/extra/templates/nginx-proxy-manager/compose.yaml @@ -0,0 +1,12 @@ +version: '3.8' +services: + nginx-proxy-manager: + image: 'jc21/nginx-proxy-manager:latest' + restart: unless-stopped + ports: + - '80:80' + - '81:81' + - '443:443' + volumes: + - ./data:/data + - ./letsencrypt:/etc/letsencrypt diff --git a/extra/templates/uptime-kuma/compose.yaml b/extra/templates/uptime-kuma/compose.yaml new file mode 100644 index 0000000..8027975 --- /dev/null +++ b/extra/templates/uptime-kuma/compose.yaml @@ -0,0 +1,9 @@ +version: '3.8' +services: + uptime-kuma: + image: louislam/uptime-kuma:1 + volumes: + - ./data:/app/data + ports: + - "3001:3001" + restart: always diff --git a/frontend/components.d.ts b/frontend/components.d.ts new file mode 100644 index 0000000..bafec4c --- /dev/null +++ b/frontend/components.d.ts @@ -0,0 +1,30 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// Generated by unplugin-vue-components +// Read more: https://github.com/vuejs/core/pull/3399 +export {} + +declare module 'vue' { + export interface GlobalComponents { + About: typeof import('./src/components/settings/About.vue')['default'] + Appearance: typeof import('./src/components/settings/Appearance.vue')['default'] + ArrayInput: typeof import('./src/components/ArrayInput.vue')['default'] + ArraySelect: typeof import('./src/components/ArraySelect.vue')['default'] + BModal: typeof import('bootstrap-vue-next')['BModal'] + Confirm: typeof import('./src/components/Confirm.vue')['default'] + Container: typeof import('./src/components/Container.vue')['default'] + General: typeof import('./src/components/settings/General.vue')['default'] + HiddenInput: typeof import('./src/components/HiddenInput.vue')['default'] + Login: typeof import('./src/components/Login.vue')['default'] + NetworkInput: typeof import('./src/components/NetworkInput.vue')['default'] + RouterLink: typeof import('vue-router')['RouterLink'] + RouterView: typeof import('vue-router')['RouterView'] + Security: typeof import('./src/components/settings/Security.vue')['default'] + StackList: typeof import('./src/components/StackList.vue')['default'] + StackListItem: typeof import('./src/components/StackListItem.vue')['default'] + Terminal: typeof import('./src/components/Terminal.vue')['default'] + TwoFADialog: typeof import('./src/components/TwoFADialog.vue')['default'] + Uptime: typeof import('./src/components/Uptime.vue')['default'] + } +} diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..bd46fe2 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,33 @@ + + + + + + + + + + + Dockge + + + + +
+ + + diff --git a/frontend/public/apple-touch-icon.png b/frontend/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..271b56a74c5ad60e2a4e8f09574bc12df498223a GIT binary patch literal 10438 zcmZvCWmFtp)9v8y?l1&<2p*ilJp>;J4#C}B0!eTS5D39t1_pxb009ypxcfkm8Qfhz z-rx83T7Ax+?$x!b>QwLAwPUq4RS0ltaRC4TftsqK?#tfzzZ)C#Wd)653IYI}iE4^+ z`TN5ITD`Xf4T(zk`C33WLxA)T<3U? zM;1oWI#vAt7j-Q``cJcWLt0_IRkdB}#OS0-DTzgc5GTUHcvX}^G40e2u9T0BuYxklTeEy&i`TtDiA_k97LV-(?z zU=Sfa3Lyu8DwgLs=;x7*i{aBz3^Y(EB80IOV+DW!H<3gY(!E^9XDk!WRmRY(Bj493 zH?KWVGt~hi+~Kncyw0VO{{2U47_D4DA&5jV+qz%X>>hc%(w{$nqPC=8%NkaJ75gE3 zEZ|W5fP{tIK?3F;lL(X8&=ZrMfgTQ^;C>DsXx!Sy#zyrt3|-tAyBh;RH{GH>~_92%xhkNTjk2uE>~KmymUxxNo8 z6kwSR*=V(|9_g$*_h5X8f8dYkgnL(WSaw4p^WhF5AtBfo%zLZ;YugXE=Wnt{bw~mU zZ%8MCjqfL$7Ix3SF)s9c#FX>sJC_m_5D-8Q($=rk4@!wY?}_hG%jkBCHfB12K)N`9 z?g4rJ1LEby#l;E*_oYrvFL@J?CyLSu2uUdnu1iC9V0Uik1UrBDP-F}>SgfrX>_LUK zw#Imm1>{|0p9t20qYA>vSn_L01gk+Z-Z-Z`|h!>L354y6{LRd1RnK64~}&9?3f4i$HL?jJ&Sts?!&%R&I17< znd`c+^~iC=oO%5m`@ZPI`e!1}boDP*?=idHnkHLC@e?JGa{@m8%ekim=Oz_Z80K|w zNGJYqva&)caDhHDp3HpBk3eo>o-~9=f8@gc6p{SN2KQoP0|+s0o(rQ22pSyD`^u7J z^enTk6;8L+7-%PJ_#cIS)3^T2Zm}&JtxBZf^AhzrEipc%bPRpxRhABVmxAe4+xNVZ zxc(>DC$ZmRza=Pih%wYKbueaPB)AooGd6c1r-Ao?J>mc}#W0*kr7v=5l9zIt#@IKI zyZ=tnJqnmyh}Ko>R#neNRh6>~H5%oA>`2fMq#+u{bZfoDvzRm+Ehs*dOSSZ_Rqss#{A;Lsm8|A=1o zW1|yxFA1lm_s~~icomTi{p%TO)r+IOPc1T`Zlpj~$l63^*f*xONEn`7AX;lhRYF^| ze=Z4rTK-IF8u3h;l8=%Y1RJZf9=H;nNLRN|pj zn0ePQ3{9$XrI@SgB~F|s@@7rhJXcH~aA78pXwaVQo^i#J)#HSX8X|qtNouU8q*fK>H7iPOEQH^1 zUi)JGxUjr<`Cn##dHr#1n#1pmZur*3Gjfrb2a?9 z&=g^0Y%CA{O?~dW4&JF1CfS%+ZYm2sup91LNZjHN7Nc~DM8q8PGIA@szoS~=L30Cy z9$?=PiqatGS_C+rih6L;C)#@Tx{Xp(nM>U(ea|46B= z;Ft07w*a}q-d{d>)mi7!_0n_eD!Vrwa333X9)r<-?4);H>8P)C+?)K#?DXFKgXeeP zT)FoR62?$J-pZEIcB3;JT>6qdeLZ#)cS>Ik53|bo3?8~+v-RaA&p5moc?Pq!QC1F= zR8&}4JCTtX?W;9_agyWX@4J>b4$Er)iXqof^?50wFM**=tm1){h{qGJ)qDAR;=+7O z6L9FH@%@(=?W$Q!G>7r?LF9RPxP3h%2?_!Ea~kA&N_RfQ6Z;&2%Rn}teHF}P2&=`2 zTT`h>515^*>Jb1R&OTlc{qLinm;FC@eJn�cmxSEMA*{aG}{ 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 literal 0 HcmV?d00001 diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..66222620953cd955c443b8db7b22041f38374476 GIT binary patch literal 5430 zcmcIoZD?E989s4B6G~}9f0ROQfv};+Gl`Lgm*L9}NyezRTJMl8t@mC#x#Z_HfmSjoMm-T(GK5bw7?0K&4m2@RJ zv7r+@+^=)a^PKm6&wJh@A$Ex-(cCO>Z5Evegm_d4aR667AKoLxH0~e6J2Vjo0lxn_ zeu|x(zze{W0NdW(Xsn&Q z;~2pA0FHeeI8lW*+*iZ=#&>M{GVna`O#mL**$;gXy(TOB#A*ysW`4gFd^wvnP2RZ8Rtc*xGozS?1nF32UA0!UoHpx zHGgD$vZO&@PiAZ}f7}l|3RGeav6c(YUIb^^<~sl4Wx4jnU!`{AwA>u~t=t%WQ?8EN zWbVdWQkng|On&tj8FzJ=@ldBB7@fINV*~o+N#Hra)YPV=677t&%ADc%#Y<9~{)3d) zk4ZW7n=&?rj>y&FBQiUBRHny|$@r8_MjdD5iu<23?CUFQ@CM+I(6wkX6@I5i!;9el zd4<0ebHydQTH)`(ygR_%0^TEa*tiZ2BS)d3Mb{9UI3~k0Z8GBg$fCg)xUv?FFLkI2 z)^cUOTnj{L{;+*Eyzn97eXYXZ4V%Ah%(>x)UGY8qzVUhcZTagpw8(_xEY_wS8tjIK{d$+ z^f8x{~`ArQ)`eLG0rk$V4B>0&hs3GDXh^LalcN_Shxxkn|{2z6nD=zisEB5b^ z>oaG!#ZL{|aEr`;@qtv`y)um)nFN0fhzCX|krOL%UfSE90{4&0{K%JTGIF}$x)Q18 zU;VOEZVtZz4UPR@{a%}TR~BHi9CDO_9|`n1&s zJ#zJ{3y8&&m0WPu!Liyew;@j2)COQ5{>88+C1cdRWdHh_j~=*v#z z!mK%FE^9un=d0q@uy?pJ zEADd*xZlLsk5_Zn-sAfdfMqT4IU8B{u;3o>g9mx>a%m4X>Jj5vgRi5xTa$fia^+Nw z%{d>A`3z9@c`Y}e@tuD7Az-lq$75c|1!gZ5mIocEOG4y>ra*pqXqNl6ng|(to5kNI zH}}|W)!}Hz^F~!pGPPh_y$(&CP?OP9)WbSO&uM&b%`N0*EODK2kKY!zX%n=khUWqL ztSTl=Exgvv(`vKQIq5Ox9l9t13FtakkEK64y^ zarqL!`kb*;&C6%5n_mEIci(o$-bp)*dl&Ek@NIzgGX45EKpWo={NK*NJJGq*w$l6! z?w6Zlm(+x4Eei2aT8RB#A@&XmvD?5d8~#ez0=w~^?bv>=x73$oJWXJ{Rt;lIv0F;p zeks$fCadKC6!^PB|A%5H|5L!;_ZYT+3fE@<`u-8%9t^kT58rv-;M(HaWXzgzYu+En zyBC3Gz_NDm%vj`lDD}bmPscpxQqiT3WZd_D3TH)ptBy_Pk$n_{>mGnHtk?K?-`G$A zdrJm20?(3B)JP%!m25CLJQs_zRw=K!pBa6>h3g50d$VWIVdi|+I+>+QGKpG;XVIYF zjQ&+LUw}b1>|WbJ_az z8L6Q@Uz_XD`%+up{U48#inpUZ?N8n&1G*2C1Z&)@A-1hW9_L;K`wQBW_Z6W0Ul#5U z0J&@ij70E*spBBQqSBSygLfCd~ U0el-o8}EK0(hmutwU+w+57cH+ZU6uP literal 0 HcmV?d00001 diff --git a/frontend/public/icon-192x192.png b/frontend/public/icon-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..aa7af8c7d913fed1c9867381b3f9344b5e02b65c GIT binary patch literal 9624 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 L7$VBGY$N^$9)cWN literal 0 HcmV?d00001 diff --git a/frontend/public/icon.svg b/frontend/public/icon.svg new file mode 100644 index 0000000..b8e2df2 --- /dev/null +++ b/frontend/public/icon.svg @@ -0,0 +1,14 @@ + + + +Created with Fabric.js 5.3.0 + + + + + + + + + + \ No newline at end of file diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json new file mode 100644 index 0000000..e62d262 --- /dev/null +++ b/frontend/public/manifest.json @@ -0,0 +1,19 @@ +{ + "name": "Dockge", + "short_name": "Dockge", + "start_url": "/", + "background_color": "#fff", + "display": "standalone", + "icons": [ + { + "src": "icon-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icon-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..aef300e --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,9 @@ + + + diff --git a/frontend/src/components/ArrayInput.vue b/frontend/src/components/ArrayInput.vue new file mode 100644 index 0000000..2271083 --- /dev/null +++ b/frontend/src/components/ArrayInput.vue @@ -0,0 +1,117 @@ + + + + + diff --git a/frontend/src/components/ArraySelect.vue b/frontend/src/components/ArraySelect.vue new file mode 100644 index 0000000..563bcbd --- /dev/null +++ b/frontend/src/components/ArraySelect.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/frontend/src/components/Confirm.vue b/frontend/src/components/Confirm.vue new file mode 100644 index 0000000..e855b67 --- /dev/null +++ b/frontend/src/components/Confirm.vue @@ -0,0 +1,84 @@ + + + diff --git a/frontend/src/components/Container.vue b/frontend/src/components/Container.vue new file mode 100644 index 0000000..3e242ec --- /dev/null +++ b/frontend/src/components/Container.vue @@ -0,0 +1,273 @@ + + + + + diff --git a/frontend/src/components/HiddenInput.vue b/frontend/src/components/HiddenInput.vue new file mode 100644 index 0000000..fb86a39 --- /dev/null +++ b/frontend/src/components/HiddenInput.vue @@ -0,0 +1,87 @@ + + + diff --git a/frontend/src/components/Login.vue b/frontend/src/components/Login.vue new file mode 100644 index 0000000..9667d7a --- /dev/null +++ b/frontend/src/components/Login.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/frontend/src/components/NetworkInput.vue b/frontend/src/components/NetworkInput.vue new file mode 100644 index 0000000..57f3c3f --- /dev/null +++ b/frontend/src/components/NetworkInput.vue @@ -0,0 +1,223 @@ + + + + + diff --git a/frontend/src/components/StackList.vue b/frontend/src/components/StackList.vue new file mode 100644 index 0000000..90edb2f --- /dev/null +++ b/frontend/src/components/StackList.vue @@ -0,0 +1,438 @@ + + + + + diff --git a/frontend/src/components/StackListItem.vue b/frontend/src/components/StackListItem.vue new file mode 100644 index 0000000..ee0a8aa --- /dev/null +++ b/frontend/src/components/StackListItem.vue @@ -0,0 +1,154 @@ + + + + + diff --git a/frontend/src/components/Terminal.vue b/frontend/src/components/Terminal.vue new file mode 100644 index 0000000..fe767a0 --- /dev/null +++ b/frontend/src/components/Terminal.vue @@ -0,0 +1,228 @@ + + + + + + + diff --git a/frontend/src/components/TwoFADialog.vue b/frontend/src/components/TwoFADialog.vue new file mode 100644 index 0000000..6ded47a --- /dev/null +++ b/frontend/src/components/TwoFADialog.vue @@ -0,0 +1,203 @@ + + + + + diff --git a/frontend/src/components/Uptime.vue b/frontend/src/components/Uptime.vue new file mode 100644 index 0000000..84c2c27 --- /dev/null +++ b/frontend/src/components/Uptime.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/frontend/src/components/settings/About.vue b/frontend/src/components/settings/About.vue new file mode 100644 index 0000000..aed0bb5 --- /dev/null +++ b/frontend/src/components/settings/About.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/frontend/src/components/settings/Appearance.vue b/frontend/src/components/settings/Appearance.vue new file mode 100644 index 0000000..c974b6e --- /dev/null +++ b/frontend/src/components/settings/Appearance.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/frontend/src/components/settings/General.vue b/frontend/src/components/settings/General.vue new file mode 100644 index 0000000..f9cf5c3 --- /dev/null +++ b/frontend/src/components/settings/General.vue @@ -0,0 +1,114 @@ + + + + diff --git a/frontend/src/components/settings/Security.vue b/frontend/src/components/settings/Security.vue new file mode 100644 index 0000000..0ece48a --- /dev/null +++ b/frontend/src/components/settings/Security.vue @@ -0,0 +1,205 @@ + + + diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts new file mode 100644 index 0000000..8799143 --- /dev/null +++ b/frontend/src/i18n.ts @@ -0,0 +1,36 @@ +// @ts-ignore Performance issue when using "vue-i18n", so we use "vue-i18n/dist/vue-i18n.esm-browser.prod.js", but typescript doesn't like that. +import { createI18n } from "vue-i18n/dist/vue-i18n.esm-browser.prod.js"; +import en from "./lang/en.json"; + +const languageList = { + +}; + +let messages = { + en, +}; + +for (let lang in languageList) { + messages[lang] = { + languageName: languageList[lang] + }; +} + +const rtlLangs = [ "fa", "ar-SY", "ur" ]; + +export const currentLocale = () => localStorage.locale + || languageList[navigator.language] && navigator.language + || languageList[navigator.language.substring(0, 2)] && navigator.language.substring(0, 2) + || "en"; + +export const localeDirection = () => { + return rtlLangs.includes(currentLocale()) ? "rtl" : "ltr"; +}; + +export const i18n = createI18n({ + locale: currentLocale(), + fallbackLocale: "en", + silentFallbackWarn: true, + silentTranslationWarn: true, + messages: messages, +}); diff --git a/frontend/src/icon.ts b/frontend/src/icon.ts new file mode 100644 index 0000000..0599e6a --- /dev/null +++ b/frontend/src/icon.ts @@ -0,0 +1,115 @@ +import { library } from "@fortawesome/fontawesome-svg-core"; +import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; + +// Add Free Font Awesome Icons +// https://fontawesome.com/v6/icons?d=gallery&p=2&s=solid&m=free +// In order to add an icon, you have to: +// 1) add the icon name in the import statement below; +// 2) add the icon name to the library.add() statement below. +import { + faArrowAltCircleUp, + faCog, + faEdit, + faEye, + faEyeSlash, + faList, + faPause, + faStop, + faPlay, + faPlus, + faSearch, + faTachometerAlt, + faTimes, + faTimesCircle, + faTrash, + faCheckCircle, + faStream, + faSave, + faExclamationCircle, + faBullhorn, + faArrowsAltV, + faUnlink, + faQuestionCircle, + faImages, + faUpload, + faCopy, + faCheck, + faFile, + faAward, + faLink, + faChevronDown, + faSignOutAlt, + faPen, + faExternalLinkSquareAlt, + faSpinner, + faUndo, + faPlusCircle, + faAngleDown, + faWrench, + faHeartbeat, + faFilter, + faInfoCircle, + faClone, + faCertificate, + faTerminal, faWarehouse, faHome, faRocket, + faRotate, + faCloudArrowDown, faArrowsRotate, +} from "@fortawesome/free-solid-svg-icons"; + +library.add( + faArrowAltCircleUp, + faCog, + faEdit, + faEye, + faEyeSlash, + faList, + faPause, + faStop, + faPlay, + faPlus, + faSearch, + faTachometerAlt, + faTimes, + faTimesCircle, + faTrash, + faCheckCircle, + faStream, + faSave, + faExclamationCircle, + faBullhorn, + faArrowsAltV, + faUnlink, + faQuestionCircle, + faImages, + faUpload, + faCopy, + faCheck, + faFile, + faAward, + faLink, + faChevronDown, + faSignOutAlt, + faPen, + faExternalLinkSquareAlt, + faSpinner, + faUndo, + faPlusCircle, + faAngleDown, + faLink, + faWrench, + faHeartbeat, + faFilter, + faInfoCircle, + faClone, + faCertificate, + faTerminal, + faWarehouse, + faHome, + faRocket, + faRotate, + faCloudArrowDown, + faArrowsRotate, +); + +export { FontAwesomeIcon }; + diff --git a/frontend/src/lang/en.json b/frontend/src/lang/en.json new file mode 100644 index 0000000..2df544b --- /dev/null +++ b/frontend/src/lang/en.json @@ -0,0 +1,53 @@ +{ + "languageName": "English", + "authIncorrectCreds": "Incorrect username or password.", + "PasswordsDoNotMatch": "Passwords do not match.", + "signedInDisp": "Signed in as {0}", + "signedInDispDisabled": "Auth Disabled.", + "home": "Home", + "console": "Console", + "registry": "Registry", + "compose": "Compose", + "addFirstStackMsg": "Compose your first stack!", + "stackName" : "Stack Name", + "deployStack": "Deploy", + "deleteStack": "Delete", + "stopStack": "Stop", + "restartStack": "Restart", + "updateStack": "Update", + "startStack": "Start", + "editStack": "Edit", + "discardStack": "Discard", + "saveStackDraft": "Save", + "notAvailableShort" : "N/A", + "deleteStackMsg": "Are you sure you want to delete this stack?", + "stackNotManagedByDockgeMsg": "This stack is not managed by Dockge.", + "primaryHostname": "Primary Hostname", + "general": "General", + "container": "Container | Containers", + "scanFolder": "Scan Stacks Folder", + "dockerImage": "Image", + "restartPolicyUnlessStopped": "Unless Stopped", + "restartPolicyAlways": "Always", + "restartPolicyOnFailure": "On Failure", + "restartPolicyNo": "No", + "environmentVariable": "Environment Variable | Environment Variables", + "restartPolicy": "Restart Policy", + "containerName": "Container Name", + "port": "Port | Ports", + "volume": "Volume | Volumes", + "network": "Network | Networks", + "dependsOn": "Container Dependency | Container Dependencies", + "addListItem": "Add {0}", + "deleteContainer": "Delete", + "addContainer": "Add Container", + "addNetwork": "Add Network", + "disableauth.message1": "Are you sure want to disable authentication?", + "disableauth.message2": "It is designed for scenarios where you intend to implement third-party authentication in front of Uptime Kuma such as Cloudflare Access, Authelia or other authentication mechanisms.", + "passwordNotMatchMsg": "The repeat password does not match.", + "autoGet": "Auto Get", + "add": "Add", + "applyToYAML": "Apply to YAML", + "createExternalNetwork": "Create", + "addInternalNetwork": "Add" +} diff --git a/frontend/src/layouts/EmptyLayout.vue b/frontend/src/layouts/EmptyLayout.vue new file mode 100644 index 0000000..825ec93 --- /dev/null +++ b/frontend/src/layouts/EmptyLayout.vue @@ -0,0 +1,8 @@ + + + + diff --git a/frontend/src/layouts/Layout.vue b/frontend/src/layouts/Layout.vue new file mode 100644 index 0000000..3d13e2a --- /dev/null +++ b/frontend/src/layouts/Layout.vue @@ -0,0 +1,304 @@ + + + + + diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..662517c --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,101 @@ +// Dayjs init inside this, so it has to be the first import +import "../../backend/util-common"; + +import { createApp, defineComponent, h } from "vue"; +import App from "./App.vue"; +import { router } from "./router"; +import { FontAwesomeIcon } from "./icon.js"; +import { i18n } from "./i18n"; + +// Dependencies +import "bootstrap"; +import Toast, { POSITION, useToast } from "vue-toastification"; +import "xterm/lib/xterm.js"; + +// CSS +import "vue-toastification/dist/index.css"; +import "xterm/css/xterm.css"; +import "./styles/main.scss"; + +// Minxins +import socket from "./mixins/socket"; +import lang from "./mixins/lang"; +import theme from "./mixins/theme"; + +const app = createApp(rootApp()); + +app.use(Toast, { + position: POSITION.BOTTOM_RIGHT, + showCloseButtonOnHover: true, +}); +app.use(router); +app.use(i18n); +app.component("FontAwesomeIcon", FontAwesomeIcon); +app.mount("#app"); + +/** + * Root Vue component + */ +function rootApp() { + const toast = useToast(); + + return defineComponent({ + mixins: [ + socket, + lang, + theme, + ], + data() { + return { + loggedIn: false, + allowLoginDialog: false, + username: null, + }; + }, + computed: { + + }, + methods: { + + /** + * Show success or error toast dependant on response status code + * @param {object} res Response object + * @returns {void} + */ + toastRes(res) { + let msg = res.msg; + if (res.msgi18n) { + if (msg != null && typeof msg === "object") { + msg = this.$t(msg.key, msg.values); + } else { + msg = this.$t(msg); + } + } + + if (res.ok) { + toast.success(msg); + } else { + toast.error(msg); + } + }, + /** + * Show a success toast + * @param {string} msg Message to show + * @returns {void} + */ + toastSuccess(msg : string) { + toast.success(this.$t(msg)); + }, + + /** + * Show an error toast + * @param {string} msg Message to show + * @returns {void} + */ + toastError(msg : string) { + toast.error(this.$t(msg)); + }, + }, + render: () => h(App), + }); +} diff --git a/frontend/src/mixins/lang.ts b/frontend/src/mixins/lang.ts new file mode 100644 index 0000000..1a9ab93 --- /dev/null +++ b/frontend/src/mixins/lang.ts @@ -0,0 +1,39 @@ +import { currentLocale } from "../i18n"; +import { setPageLocale } from "../util-frontend"; +import { defineComponent } from "vue"; +const langModules = import.meta.glob("../lang/*.json"); + +export default defineComponent({ + data() { + return { + language: currentLocale(), + }; + }, + + watch: { + async language(lang) { + await this.changeLang(lang); + }, + }, + + async created() { + if (this.language !== "en") { + await this.changeLang(this.language); + } + }, + + methods: { + /** + * Change the application language + * @param {string} lang Language code to switch to + * @returns {Promise} + */ + async changeLang(lang : string) { + const message = (await langModules["../lang/" + lang + ".json"]()).default; + this.$i18n.setLocaleMessage(lang, message); + this.$i18n.locale = lang; + localStorage.locale = lang; + setPageLocale(); + } + } +}); diff --git a/frontend/src/mixins/socket.ts b/frontend/src/mixins/socket.ts new file mode 100644 index 0000000..42e4698 --- /dev/null +++ b/frontend/src/mixins/socket.ts @@ -0,0 +1,317 @@ +import { io } from "socket.io-client"; +import { Socket } from "socket.io-client"; +import { defineComponent } from "vue"; +import jwtDecode from "jwt-decode"; +import { Terminal } from "xterm"; + +let socket : Socket; + +let terminalMap : Map = new Map(); + +export default defineComponent({ + data() { + return { + socketIO: { + token: null, + firstConnect: true, + connected: false, + connectCount: 0, + initedSocketIO: false, + connectionErrorMsg: `${this.$t("Cannot connect to the socket server.")} ${this.$t("Reconnecting...")}`, + showReverseProxyGuide: true, + }, + info: { + + }, + remember: (localStorage.remember !== "0"), + loggedIn: false, + allowLoginDialog: false, + username: null, + stackList: {}, + composeTemplate: "", + }; + }, + computed: { + usernameFirstChar() { + if (typeof this.username == "string" && this.username.length >= 1) { + return this.username.charAt(0).toUpperCase(); + } else { + return "🐻"; + } + }, + + /** + * Frontend Version + * It should be compiled to a static value while building the frontend. + * Please see ./frontend/vite.config.ts, it is defined via vite.js + * @returns {string} + */ + frontendVersion() { + // eslint-disable-next-line no-undef + return FRONTEND_VERSION; + }, + + /** + * Are both frontend and backend in the same version? + * @returns {boolean} + */ + isFrontendBackendVersionMatched() { + if (!this.info.version) { + return true; + } + return this.info.version === this.frontendVersion; + }, + + }, + watch: { + remember() { + localStorage.remember = (this.remember) ? "1" : "0"; + }, + + // Reload the SPA if the server version is changed. + "info.version"(to, from) { + if (from && from !== to) { + window.location.reload(); + } + }, + }, + created() { + this.initSocketIO(); + }, + mounted() { + return; + + }, + methods: { + /** + * Initialize connection to socket server + * @param bypass Should the check for if we + * are on a status page be bypassed? + */ + initSocketIO(bypass = false) { + // No need to re-init + if (this.socketIO.initedSocketIO) { + return; + } + + this.socketIO.initedSocketIO = true; + let url : string; + const env = process.env.NODE_ENV || "production"; + if (env === "development" || localStorage.dev === "dev") { + url = location.protocol + "//" + location.hostname + ":5001"; + } else { + url = location.protocol + "//" + location.host; + } + + socket = io(url, { + transports: [ "websocket", "polling" ] + }); + + socket.on("connect", () => { + console.log("Connected to the socket server"); + + this.socketIO.connectCount++; + this.socketIO.connected = true; + this.socketIO.showReverseProxyGuide = false; + const token = this.storage().token; + + if (token) { + if (token !== "autoLogin") { + console.log("Logging in by token"); + this.loginByToken(token); + } else { + // Timeout if it is not actually auto login + setTimeout(() => { + if (! this.loggedIn) { + this.allowLoginDialog = true; + this.storage().removeItem("token"); + } + }, 5000); + } + } else { + this.allowLoginDialog = true; + } + + this.socketIO.firstConnect = false; + }); + + socket.on("disconnect", () => { + console.log("disconnect"); + this.socketIO.connectionErrorMsg = "Lost connection to the socket server. Reconnecting..."; + this.socketIO.connected = false; + }); + + socket.on("connect_error", (err) => { + console.error(`Failed to connect to the backend. Socket.io connect_error: ${err.message}`); + this.socketIO.connectionErrorMsg = `${this.$t("Cannot connect to the socket server.")} [${err}] ${this.$t("Reconnecting...")}`; + this.socketIO.showReverseProxyGuide = true; + this.socketIO.connected = false; + this.socketIO.firstConnect = false; + }); + + // Custom Events + + socket.on("info", (info) => { + this.info = info; + }); + + socket.on("autoLogin", () => { + this.loggedIn = true; + this.storage().token = "autoLogin"; + this.socketIO.token = "autoLogin"; + this.allowLoginDialog = false; + this.afterLogin(); + }); + + socket.on("setup", () => { + console.log("setup"); + this.$router.push("/setup"); + }); + + socket.on("terminalWrite", (terminalName, data) => { + const terminal = terminalMap.get(terminalName); + if (!terminal) { + //console.error("Terminal not found: " + terminalName); + return; + } + terminal.write(data); + }); + + socket.on("stackList", (res) => { + if (res.ok) { + this.stackList = res.stackList; + } + }); + + socket.on("stackStatusList", (res) => { + if (res.ok) { + for (let stackName in res.stackStatusList) { + const stackObj = this.stackList[stackName]; + if (stackObj) { + stackObj.status = res.stackStatusList[stackName]; + } + } + } + }); + }, + + /** + * The storage currently in use + * @returns Current storage + */ + storage() : Storage { + return (this.remember) ? localStorage : sessionStorage; + }, + + getSocket() : Socket { + return socket; + }, + + /** + * Get payload of JWT cookie + * @returns {(object | undefined)} JWT payload + */ + getJWTPayload() { + const jwtToken = this.storage().token; + + if (jwtToken && jwtToken !== "autoLogin") { + return jwtDecode(jwtToken); + } + return undefined; + }, + + /** + * Send request to log user in + * @param {string} username Username to log in with + * @param {string} password Password to log in with + * @param {string} token User token + * @param {loginCB} callback Callback to call with result + * @returns {void} + */ + login(username : string, password : string, token : string, callback) { + this.getSocket().emit("login", { + username, + password, + token, + }, (res) => { + if (res.tokenRequired) { + callback(res); + } + + if (res.ok) { + this.storage().token = res.token; + this.socketIO.token = res.token; + this.loggedIn = true; + this.username = this.getJWTPayload()?.username; + + this.afterLogin(); + + // Trigger Chrome Save Password + history.pushState({}, ""); + } + + callback(res); + }); + }, + + /** + * Log in using a token + * @param {string} token Token to log in with + * @returns {void} + */ + loginByToken(token : string) { + socket.emit("loginByToken", token, (res) => { + this.allowLoginDialog = true; + + if (! res.ok) { + this.logout(); + } else { + this.loggedIn = true; + this.username = this.getJWTPayload()?.username; + this.afterLogin(); + } + }); + }, + + /** + * Log out of the web application + * @returns {void} + */ + logout() { + socket.emit("logout", () => { }); + this.storage().removeItem("token"); + this.socketIO.token = null; + this.loggedIn = false; + this.username = null; + this.clearData(); + }, + + /** + * @returns {void} + */ + clearData() { + + }, + + afterLogin() { + + }, + + bindTerminal(terminalName : string, terminal : Terminal) { + // Load terminal, get terminal screen + socket.emit("terminalJoin", terminalName, (res) => { + if (res.ok) { + terminal.write(res.buffer); + terminalMap.set(terminalName, terminal); + } else { + this.toastRes(res); + } + }); + }, + + unbindTerminal(terminalName : string) { + terminalMap.delete(terminalName); + }, + + } +}); diff --git a/frontend/src/mixins/theme.ts b/frontend/src/mixins/theme.ts new file mode 100644 index 0000000..3982d04 --- /dev/null +++ b/frontend/src/mixins/theme.ts @@ -0,0 +1,80 @@ +import { defineComponent } from "vue"; + +export default defineComponent({ + data() { + return { + system: (window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "light", + userTheme: localStorage.theme, + statusPageTheme: "light", + forceStatusPageTheme: false, + path: "", + }; + }, + + computed: { + theme() { + if (this.userTheme === "auto") { + return this.system; + } + return this.userTheme; + }, + + isDark() { + return this.theme === "dark"; + } + }, + + watch: { + "$route.fullPath"(path) { + this.path = path; + }, + + userTheme(to, from) { + localStorage.theme = to; + }, + + styleElapsedTime(to, from) { + localStorage.styleElapsedTime = to; + }, + + theme(to, from) { + document.body.classList.remove(from); + document.body.classList.add(this.theme); + this.updateThemeColorMeta(); + }, + + userHeartbeatBar(to, from) { + localStorage.heartbeatBarTheme = to; + }, + + heartbeatBarTheme(to, from) { + document.body.classList.remove(from); + document.body.classList.add(this.heartbeatBarTheme); + } + }, + + mounted() { + // Default Dark + if (! this.userTheme) { + this.userTheme = "dark"; + } + + document.body.classList.add(this.theme); + this.updateThemeColorMeta(); + }, + + methods: { + /** + * Update the theme color meta tag + * @returns {void} + */ + updateThemeColorMeta() { + if (this.theme === "dark") { + document.querySelector("#theme-color").setAttribute("content", "#161B22"); + } else { + document.querySelector("#theme-color").setAttribute("content", "#5cdd8b"); + } + } + } +}); + diff --git a/frontend/src/pages/Compose.vue b/frontend/src/pages/Compose.vue new file mode 100644 index 0000000..1a12e78 --- /dev/null +++ b/frontend/src/pages/Compose.vue @@ -0,0 +1,599 @@ + + + + + diff --git a/frontend/src/pages/Console.vue b/frontend/src/pages/Console.vue new file mode 100644 index 0000000..95f9977 --- /dev/null +++ b/frontend/src/pages/Console.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/frontend/src/pages/ContainerTerminal.vue b/frontend/src/pages/ContainerTerminal.vue new file mode 100644 index 0000000..3eb4f7f --- /dev/null +++ b/frontend/src/pages/ContainerTerminal.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/frontend/src/pages/Dashboard.vue b/frontend/src/pages/Dashboard.vue new file mode 100644 index 0000000..7b653af --- /dev/null +++ b/frontend/src/pages/Dashboard.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/frontend/src/pages/DashboardHome.vue b/frontend/src/pages/DashboardHome.vue new file mode 100644 index 0000000..d32ce45 --- /dev/null +++ b/frontend/src/pages/DashboardHome.vue @@ -0,0 +1,231 @@ + + + + + diff --git a/frontend/src/pages/Settings.vue b/frontend/src/pages/Settings.vue new file mode 100644 index 0000000..e25cac7 --- /dev/null +++ b/frontend/src/pages/Settings.vue @@ -0,0 +1,252 @@ + + + + + diff --git a/frontend/src/pages/Setup.vue b/frontend/src/pages/Setup.vue new file mode 100644 index 0000000..d8ec406 --- /dev/null +++ b/frontend/src/pages/Setup.vue @@ -0,0 +1,138 @@ + + + + + diff --git a/frontend/src/router.ts b/frontend/src/router.ts new file mode 100644 index 0000000..0c0d65c --- /dev/null +++ b/frontend/src/router.ts @@ -0,0 +1,90 @@ +import { createRouter, createWebHistory } from "vue-router"; + +import Layout from "./layouts/Layout.vue"; +import Setup from "./pages/Setup.vue"; +import Dashboard from "./pages/Dashboard.vue"; +import DashboardHome from "./pages/DashboardHome.vue"; +import Console from "./pages/Console.vue"; +import Compose from "./pages/Compose.vue"; +import ContainerTerminal from "./pages/ContainerTerminal.vue"; + +const Settings = () => import("./pages/Settings.vue"); + +// Settings - Sub Pages +import Appearance from "./components/settings/Appearance.vue"; +import General from "./components/settings/General.vue"; +const Security = () => import("./components/settings/Security.vue"); +import About from "./components/settings/About.vue"; + +const routes = [ + { + path: "/empty", + component: Layout, + children: [ + { + path: "", + component: Dashboard, + children: [ + { + name: "DashboardHome", + path: "/", + component: DashboardHome, + children: [ + { + path: "/compose", + component: Compose, + }, + { + path: "/compose/:stackName", + name: "compose", + component: Compose, + props: true, + }, + { + path: "/terminal/:stackName/:serviceName/:type", + component: ContainerTerminal, + name: "containerTerminal", + }, + ] + }, + { + path: "/console", + component: Console, + }, + { + path: "/settings", + component: Settings, + children: [ + { + path: "general", + component: General, + }, + { + path: "appearance", + component: Appearance, + }, + { + path: "security", + component: Security, + }, + { + path: "about", + component: About, + }, + ] + }, + ] + }, + ] + }, + { + path: "/setup", + component: Setup, + }, +]; + +export const router = createRouter({ + linkActiveClass: "active", + history: createWebHistory(), + routes, +}); diff --git a/frontend/src/styles/localization.scss b/frontend/src/styles/localization.scss new file mode 100644 index 0000000..97be377 --- /dev/null +++ b/frontend/src/styles/localization.scss @@ -0,0 +1,9 @@ +html[lang='fa'] { + #app { + font-family: 'IRANSans', 'Iranian Sans','B Nazanin', 'Tahoma', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, segoe ui, Roboto, helvetica neue, Arial, noto sans, sans-serif, apple color emoji, segoe ui emoji, segoe ui symbol, noto color emoji; + } +} + +ul.multiselect__content { + padding-left: 0 !important; +} diff --git a/frontend/src/styles/main.scss b/frontend/src/styles/main.scss new file mode 100644 index 0000000..e69cac3 --- /dev/null +++ b/frontend/src/styles/main.scss @@ -0,0 +1,697 @@ +@import "vars.scss"; +@import "bootstrap/scss/bootstrap"; +@import "bootstrap-vue-next/dist/bootstrap-vue-next.css"; + +#app { + font-family: BlinkMacSystemFont, segoe ui, Roboto, helvetica neue, Arial, noto sans, sans-serif, apple color emoji, segoe ui emoji, segoe ui symbol, noto color emoji; +} + +h1 { + font-size: 32px; +} + +h2 { + font-size: 26px; +} + +textarea.form-control { + border-radius: 19px; +} + +::-webkit-scrollbar { + width: 10px; +} + +.bg-maintenance { + color: white !important; + background-color: $maintenance !important; +} + +.bg-dark { + color: white; +} + +.text-maintenance { + color: $maintenance !important; +} + +::placeholder { + color: $dark-font-color3 !important; +} + +.incident a, +.bg-maintenance a { + color: inherit; +} + +.list-group { + border-radius: 0.75rem; + + .dark & { + .list-group-item { + background-color: $dark-bg2; + color: $dark-font-color; + border-color: $dark-border-color; + } + } +} + + +// optgroup +optgroup { + color: #b1b1b1; + option { + color: #212529; + } +} + +.dark { + optgroup { + color: #535864; + option { + color: $dark-font-color; + } + } +} + +// Scrollbar +::-webkit-scrollbar-thumb { + background: #ccc; + border-radius: 20px; +} + +.modal { + backdrop-filter: blur(3px); +} + +.modal-content { + border-radius: 1rem; + box-shadow: 0 15px 70px rgba(0, 0, 0, 0.1); + + .dark & { + box-shadow: 0 15px 70px rgb(0 0 0); + background-color: $dark-bg; + } +} + +.VuePagination__count { + font-size: 13px; + text-align: center; +} + +.shadow-box { + box-shadow: 0 15px 70px rgba(0, 0, 0, 0.1); + padding: 10px; + border-radius: 10px; + + &.big-padding { + padding: 20px; + } +} + +.btn { + padding-left: 20px; + padding-right: 20px; +} + +.btn-sm { + border-radius: 25px; +} + +.btn-primary { + color: white; + background: $primary-gradient; + + &:hover, &:active, &:focus, &.active { + color: white; + background: $primary-gradient-active; + border-color: $highlight; + } + + .dark & { + color: $dark-font-color2; + } +} + +.btn-normal { + $bg-color: #F5F5F5; + + background-color: $bg-color; + border-color: $bg-color; + + &:hover { + $hover-color: darken($bg-color, 3%); + background-color: $hover-color; + border-color: $hover-color; + } +} + +.btn-warning { + color: white; + + &:hover, &:active, &:focus, &.active { + color: white; + } +} + +.btn-info { + color: white; + + &:hover, &:active, &:focus, &.active { + color: white; + } +} + +.btn-dark { + background-color: #161B22; +} + +.btn-outline-normal { + padding: 4px 10px; + border: 1px solid #ced4da; + border-radius: 25px; + background-color: transparent; + + .dark & { + color: $dark-font-color; + border: 1px solid $dark-font-color2; + } + + &.active { + background-color: $highlight-white; + + .dark & { + background-color: $dark-font-color2; + } + } +} + +@media (max-width: 550px) { + .table-shadow-box { + padding: 10px !important; + + thead { + display: none; + } + + tbody { + .shadow-box { + background-color: white; + } + } + + tr { + margin-top: 0 !important; + padding: 4px 10px !important; + display: block; + margin-bottom: 6px; + + td:first-child { + font-weight: bold; + } + + td:nth-child(-n+3) { + text-align: center; + } + + td:last-child { + text-align: left; + } + + td { + border-bottom: 1px solid $dark-font-color; + display: block; + padding: 4px; + + .badge { + margin: auto; + display: block; + width: 30%; + } + } + } + } +} + +// Dark Theme override here +.dark { + background-color: #090c10; + color: $dark-font-color; + + mark, .mark { + background-color: #b6ad86; + } + + &::-webkit-scrollbar-thumb, ::-webkit-scrollbar-thumb { + background: $dark-border-color; + } + + .shadow-box { + &:not(.alert) { + background-color: $dark-bg; + } + } + + .form-check-input { + background-color: $dark-bg2; + border-color: $dark-border-color; + } + + .input-group-text { + background-color: #282f39; + border-color: $dark-border-color; + color: $dark-font-color; + } + + .form-check-input:checked { + border-color: $primary; // Re-apply bootstrap border + } + + .form-switch .form-check-input { + background-color: #232f3b; + } + + a:not(.btn), + .table, + .nav-link { + color: $dark-font-color; + + &.btn-info { + color: white; + } + } + + .incident a, + .bg-maintenance a { + color: inherit; + } + + .form-control, + .form-control:focus, + .form-select, + .form-select:focus { + color: $dark-font-color; + background-color: $dark-bg2; + } + + .form-select:disabled { + color: rgba($dark-font-color, 0.7); + background-color: $dark-bg; + } + + .form-control, .form-select { + border-color: $dark-border-color; + } + + .form-control:disabled, .form-control[readonly] { + background-color: #232f3b; + opacity: 1; + } + + .table-hover > tbody > tr:hover > * { + --bs-table-accent-bg: #070a10; + color: $dark-font-color; + } + + .nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: $dark-font-color2; + background: $primary-gradient; + + &:hover { + background: $primary-gradient-active; + } + } + + .bg-primary { + color: $dark-font-color2; + background: $primary-gradient; + } + + .btn-secondary { + color: white; + } + + .btn-normal { + $bg-color: $dark-header-bg; + + color: $dark-font-color; + background-color: $bg-color; + border-color: $bg-color; + + &:hover { + $hover-color: darken($bg-color, 3%); + background-color: $hover-color; + border-color: $hover-color; + } + } + + .btn-warning { + color: $dark-font-color2; + + &:hover, &:active, &:focus, &.active { + color: $dark-font-color2; + } + } + + .btn-close { + box-shadow: none; + filter: invert(1); + + &:hover { + opacity: 0.6; + } + } + + .modal-header { + border-color: $dark-bg; + } + + .modal-footer { + border-color: $dark-bg; + } + + // Pagination + .page-item.disabled .page-link { + background-color: $dark-bg; + border-color: $dark-border-color; + } + + .page-link { + background-color: $dark-bg; + border-color: $dark-border-color; + color: $dark-font-color; + } + + .stack-list { + .item { + &:hover { + background-color: $dark-bg2; + } + + &.active { + background-color: $dark-bg2; + } + } + } + + @media (max-width: 550px) { + .table-shadow-box { + tbody { + .shadow-box { + background-color: $dark-bg2; + + td { + border-bottom: 1px solid $dark-border-color; + } + } + } + } + } + + .alert { + &.bg-info, + &.bg-warning, + &.bg-danger, + &.bg-maintenance, + &.bg-light { + color: $dark-font-color2; + } + } +} + +// Floating Label +.form-floating > .form-control:focus ~ label::after, .form-floating > .form-control:not(:placeholder-shown) ~ label::after, .form-floating > .form-control-plaintext ~ label::after, .form-floating > .form-select ~ label::after { + background-color: transparent; + + +} +.form-floating > label { + .dark & { + color: $dark-font-color3 !important; + } +} + + +/* + * Transitions + */ + +// page-change +.slide-fade-enter-active { + transition: all 0.2s $easing-in; +} + +.slide-fade-leave-active { + transition: all 0.2s $easing-in; +} + +.slide-fade-enter-from, +.slide-fade-leave-to { + transform: translateY(50px); + opacity: 0; +} + +.slide-fade-right-enter-active { + transition: all 0.2s $easing-in; +} + +.slide-fade-right-leave-active { + transition: all 0.2s $easing-in; +} + +.slide-fade-right-enter-from, +.slide-fade-right-leave-to { + transform: translateX(50px); + opacity: 0; +} + +.slide-fade-up-enter-active { + transition: all 0.2s $easing-in; +} + +.slide-fade-up-leave-active { + transition: all 0.2s $easing-in; +} + +.slide-fade-up-enter-from, +.slide-fade-up-leave-to { + transform: translateY(-50px); + opacity: 0; +} + +.stack-list { + &.scrollbar { + overflow-y: auto; + } + + @media (max-width: 770px) { + &.scrollbar { + height: calc(100% - 97px); + } + } + + .item { + display: flex; + align-items: center; + height: 52px; + text-decoration: none; + border-radius: 10px; + transition: all ease-in-out 0.15s; + width: 100%; + padding: 0 8px; + + &.disabled { + opacity: 0.3; + } + + &:hover { + background-color: $highlight-white; + } + + &.active { + background-color: #cdf8f4; + } + + .title { + display: inline-block; + margin-top: -4px; + } + } +} + +.alert-success { + color: #122f21; + background-color: $primary; + border-color: $primary; +} + +.alert-info { + color: #055160; + background-color: #cff4fc; + border-color: #cff4fc; +} + +.alert-danger { + color: #842029; + background-color: #f8d7da; + border-color: #f8d7da; +} + +.btn-success { + color: #fff; + background-color: #4caf50; + border-color: #4caf50; +} + + +[contenteditable=true] { + transition: all $easing-in 0.2s; + background-color: rgba(239, 239, 239, 0.7); + border-radius: 8px; + + &.no-bg { + background-color: transparent !important; + } + + &:focus { + outline: 0 solid #eee; + background-color: rgba(245, 245, 245, 0.9); + } + + &:hover { + background-color: rgba(239, 239, 239, 0.8); + } + + .dark & { + background-color: rgba(239, 239, 239, 0.2); + } + + /* + &::after { + margin-left: 5px; + content: "🖊️"; + font-size: 13px; + color: #eee; + } + */ + +} + +.action { + transition: all $easing-in 0.2s; + + &:hover { + cursor: pointer; + transform: scale(1.2); + } +} + +.vue-image-crop-upload .vicp-wrap { + border-radius: 10px !important; +} + +.spinner { + color: $primary; +} + +.prism-editor__textarea { + outline: none !important; +} + +h5.settings-subheading::after { + content: ""; + display: block; + width: 50%; + padding-top: 8px; + border-bottom: 1px solid $dark-border-color; +} + +/* required class */ +.code-editor, .css-editor { + /* we dont use `language-` classes anymore so thats why we need to add background and text color manually */ + + border-radius: 1rem; + padding: 10px 5px; + border: 1px solid #ced4da; + + .dark & { + background: $dark-bg2; + border: 1px solid $dark-border-color; + } +} + + +$shadow-box-padding: 20px; + +.shadow-box-with-fixed-bottom-bar { + padding-top: $shadow-box-padding; + padding-bottom: 0; + padding-right: $shadow-box-padding; + padding-left: $shadow-box-padding; +} + +.fixed-bottom-bar { + position: sticky; + bottom: 0; + margin-left: -$shadow-box-padding; + margin-right: -$shadow-box-padding; + z-index: 100; + background-color: rgba(white, 0.2); + backdrop-filter: blur(2px); + border-radius: 0 0 10px 10px; + + .dark & { + background-color: rgba($dark-header-bg, 0.9); + } +} + +@media (max-width: 770px) { + .toast-container { + margin-bottom: 100px !important; + } +} + +@media (max-width: 550px) { + .toast-container { + margin-bottom: 126px !important; + } +} + +.main-terminal { + .xterm-viewport { + border-radius: 10px; + background-color: $dark-bg !important; + } +} + +code { + padding: .2em .4em; + margin: 0; + font-size: 85%; + white-space: break-spaces; + background-color: rgba(239, 239, 239, 0.15); + + border-radius: 6px; + font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + color: black; + + .dark & { + color: $dark-font-color; + } +} + +// Vue Prism Editor bug - workaround +// https://github.com/koca/vue-prism-editor/issues/87 +/* +.prism-editor__textarea { + width: 999999px !important; +} +.prism-editor__editor { + white-space: pre !important; +} +.prism-editor__container { + overflow-x: scroll !important; +}*/ + +// Localization +@import "localization.scss"; diff --git a/frontend/src/styles/vars.scss b/frontend/src/styles/vars.scss new file mode 100644 index 0000000..5595da2 --- /dev/null +++ b/frontend/src/styles/vars.scss @@ -0,0 +1,26 @@ +$primary: #74c2ff; +$danger: #dc3545; +$warning: #f8a306; +$maintenance: #1747f5; +$link-color: #111; +$border-radius: 50rem; + +$highlight: #9dd1ff; +$highlight-white: #e7faec; + +$dark-font-color: #b1b8c0; +$dark-font-color2: #020b05; +$dark-font-color3: #575c62; +$dark-bg: #0d1117; +$dark-bg2: #070a10; +$dark-border-color: #1d2634; +$dark-header-bg: #161b22; + +$easing-in: cubic-bezier(0.54, 0.78, 0.55, 0.97); +$easing-out: cubic-bezier(0.25, 0.46, 0.45, 0.94); +$easing-in-out: cubic-bezier(0.79, 0.14, 0.15, 0.86); + +$dropdown-border-radius: 0.5rem; + +$primary-gradient: linear-gradient(135deg, #74c2ff 0%, #74c2ff 75%, #86e6a9); +$primary-gradient-active: linear-gradient(135deg, #74c2ff 0%, #74c2ff 50%, #86e6a9); diff --git a/frontend/src/util-frontend.ts b/frontend/src/util-frontend.ts new file mode 100644 index 0000000..e55ebf0 --- /dev/null +++ b/frontend/src/util-frontend.ts @@ -0,0 +1,215 @@ +import dayjs from "dayjs"; +import timezones from "timezones-list"; +import { localeDirection, currentLocale } from "./i18n"; +import { POSITION } from "vue-toastification"; + +/** + * Returns the offset from UTC in hours for the current locale. + * @param {string} timeZone Timezone to get offset for + * @returns {number} The offset from UTC in hours. + * + * Generated by Trelent + */ +function getTimezoneOffset(timeZone) { + const now = new Date(); + const tzString = now.toLocaleString("en-US", { + timeZone, + }); + const localString = now.toLocaleString("en-US"); + const diff = (Date.parse(localString) - Date.parse(tzString)) / 3600000; + const offset = diff + now.getTimezoneOffset() / 60; + return -offset; +} + +/** + * Returns a list of timezones sorted by their offset from UTC. + * @returns {object[]} A list of the given timezones sorted by their offset from UTC. + * + * Generated by Trelent + */ +export function timezoneList() { + let result = []; + + for (let timezone of timezones) { + try { + let display = dayjs().tz(timezone.tzCode).format("Z"); + + result.push({ + name: `(UTC${display}) ${timezone.tzCode}`, + value: timezone.tzCode, + time: getTimezoneOffset(timezone.tzCode), + }); + } catch (e) { + // Skipping not supported timezone.tzCode by dayjs + } + } + + result.sort((a, b) => { + if (a.time > b.time) { + return 1; + } + + if (b.time > a.time) { + return -1; + } + + return 0; + }); + + return result; +} + +/** + * Set the locale of the HTML page + * @returns {void} + */ +export function setPageLocale() { + const html = document.documentElement; + html.setAttribute("lang", currentLocale() ); + html.setAttribute("dir", localeDirection() ); +} + +/** + * Get the base URL + * Mainly used for dev, because the backend and the frontend are in different ports. + * @returns {string} Base URL + */ +export function getResBaseURL() { + const env = process.env.NODE_ENV; + if (env === "development" && isDevContainer()) { + return location.protocol + "//" + getDevContainerServerHostname(); + } else if (env === "development" || localStorage.dev === "dev") { + return location.protocol + "//" + location.hostname + ":3001"; + } else { + return ""; + } +} + +/** + * Are we currently running in a dev container? + * @returns {boolean} Running in dev container? + */ +export function isDevContainer() { + // eslint-disable-next-line no-undef + return (typeof DEVCONTAINER === "string" && DEVCONTAINER === "1"); +} + +/** + * Supports GitHub Codespaces only currently + * @returns {string} Dev container server hostname + */ +export function getDevContainerServerHostname() { + if (!isDevContainer()) { + return ""; + } + + // eslint-disable-next-line no-undef + return CODESPACE_NAME + "-3001." + GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN; +} + +/** + * Regex pattern fr identifying hostnames and IP addresses + * @param {boolean} mqtt whether or not the regex should take into + * account the fact that it is an mqtt uri + * @returns {RegExp} The requested regex + */ +export function hostNameRegexPattern(mqtt = false) { + // mqtt, mqtts, ws and wss schemes accepted by mqtt.js (https://github.com/mqttjs/MQTT.js/#connect) + const mqttSchemeRegexPattern = "((mqtt|ws)s?:\\/\\/)?"; + // Source: https://digitalfortress.tech/tips/top-15-commonly-used-regex/ + const ipRegexPattern = `((^${mqtt ? mqttSchemeRegexPattern : ""}((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))$)|(^((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?$))`; + // Source: https://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address + const hostNameRegexPattern = `^${mqtt ? mqttSchemeRegexPattern : ""}([a-zA-Z0-9])?(([a-zA-Z0-9_]|[a-zA-Z0-9_][a-zA-Z0-9\\-_]*[a-zA-Z0-9_])\\.)*([A-Za-z0-9_]|[A-Za-z0-9_][A-Za-z0-9\\-_]*[A-Za-z0-9_])(\\.)?$`; + + return `${ipRegexPattern}|${hostNameRegexPattern}`; +} + +/** + * Get the tag color options + * Shared between components + * @param {any} self Component + * @returns {object[]} Colour options + */ +export function colorOptions(self) { + return [ + { name: self.$t("Gray"), + color: "#4B5563" }, + { name: self.$t("Red"), + color: "#DC2626" }, + { name: self.$t("Orange"), + color: "#D97706" }, + { name: self.$t("Green"), + color: "#059669" }, + { name: self.$t("Blue"), + color: "#2563EB" }, + { name: self.$t("Indigo"), + color: "#4F46E5" }, + { name: self.$t("Purple"), + color: "#7C3AED" }, + { name: self.$t("Pink"), + color: "#DB2777" }, + ]; +} + +/** + * Loads the toast timeout settings from storage. + * @returns {object} The toast plugin options object. + */ +export function loadToastSettings() { + return { + position: POSITION.BOTTOM_RIGHT, + containerClassName: "toast-container", + showCloseButtonOnHover: true, + + filterBeforeCreate: (toast, toasts) => { + if (toast.timeout === 0) { + return false; + } else { + return toast; + } + }, + }; +} + +/** + * Get timeout for success toasts + * @returns {(number|boolean)} Timeout in ms. If false timeout disabled. + */ +export function getToastSuccessTimeout() { + let successTimeout = 20000; + + if (localStorage.toastSuccessTimeout !== undefined) { + const parsedTimeout = parseInt(localStorage.toastSuccessTimeout); + if (parsedTimeout != null && !Number.isNaN(parsedTimeout)) { + successTimeout = parsedTimeout; + } + } + + if (successTimeout === -1) { + successTimeout = false; + } + + return successTimeout; +} + +/** + * Get timeout for error toasts + * @returns {(number|boolean)} Timeout in ms. If false timeout disabled. + */ +export function getToastErrorTimeout() { + let errorTimeout = -1; + + if (localStorage.toastErrorTimeout !== undefined) { + const parsedTimeout = parseInt(localStorage.toastErrorTimeout); + if (parsedTimeout != null && !Number.isNaN(parsedTimeout)) { + errorTimeout = parsedTimeout; + } + } + + if (errorTimeout === -1) { + errorTimeout = false; + } + + return errorTimeout; +} + diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..899b0bc --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1,7 @@ +/// + +declare module "*.vue" { + import type { DefineComponent } from "vue"; + const component: DefineComponent<{}, {}, any>; + export default component; +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..a640d82 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,36 @@ +import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; +import Components from "unplugin-vue-components/vite"; +import { BootstrapVueNextResolver } from "unplugin-vue-components/resolvers"; +import viteCompression from "vite-plugin-compression"; +import "vue"; + +const viteCompressionFilter = /\.(js|mjs|json|css|html|svg)$/i; + +// https://vitejs.dev/config/ +export default defineConfig({ + server: { + port: 5000, + }, + define: { + "FRONTEND_VERSION": JSON.stringify(process.env.npm_package_version), + }, + root: "./frontend", + build: { + outDir: "../frontend-dist", + }, + plugins: [ + vue(), + Components({ + resolvers: [ BootstrapVueNextResolver() ], + }), + viteCompression({ + algorithm: "gzip", + filter: viteCompressionFilter, + }), + viteCompression({ + algorithm: "brotliCompress", + filter: viteCompressionFilter, + }), + ], +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..3175afc --- /dev/null +++ b/package.json @@ -0,0 +1,79 @@ +{ + "name": "dockge", + "version": "1.0.0", + "type": "module", + "scripts": { + "fmt": "eslint \"**/*.{ts,vue}\" --fix", + "lint": "eslint \"**/*.{ts,vue}\"", + "start": "tsx ./backend/index.ts", + "dev:backend": "cross-env NODE_ENV=development tsx watch ./backend/index.ts", + "dev:frontend": "cross-env NODE_ENV=development vite --host --config ./frontend/vite.config.ts", + "build:frontend": "vite build --config ./frontend/vite.config.ts", + "build:docker-base": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/dockge:base -f ./docker/Base.Dockerfile . --push", + "build:docker": "pnpm run build:frontend && docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/dockge:latest -t louislam/dockge:1 -t louislam/dockge:1.0.0 -f ./docker/Dockerfile . --push", + "build:docker-nightly": "pnpm run build:frontend && docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t louislam/dockge:nightly --target nightly -f ./docker/Dockerfile . --push", + "start-docker": "docker run --rm -p 5001:5001 --name dockge louislam/dockge:latest", + "mark-as-nightly": "tsx ./extra/mark-as-nightly.ts" + }, + "dependencies": { + "@homebridge/node-pty-prebuilt-multiarch": "~0.11.10", + "@louislam/sqlite3": "~15.1.6", + "bcryptjs": "~2.4.3", + "check-password-strength": "~2.0.7", + "command-exists": "~1.2.9", + "compare-versions": "~6.1.0", + "composerize": "~1.4.1", + "croner": "~7.0.4", + "dayjs": "~1.11.10", + "express": "~4.18.2", + "express-static-gzip": "~2.1.7", + "http-graceful-shutdown": "~3.1.13", + "jsonwebtoken": "~9.0.2", + "jwt-decode": "~3.1.2", + "knex": "~2.5.1", + "limiter-es6-compat": "~2.1.2", + "mysql2": "^3.6.3", + "redbean-node": "0.3.2", + "socket.io": "~4.7.2", + "socket.io-client": "~4.7.2", + "timezones-list": "~3.0.2", + "ts-command-line-args": "~2.5.1", + "tsx": "~3.14.0", + "type-fest": "~4.3.3", + "yaml": "~2.3.4" + }, + "devDependencies": { + "@fortawesome/fontawesome-svg-core": "6.4.2", + "@fortawesome/free-regular-svg-icons": "6.4.2", + "@fortawesome/free-solid-svg-icons": "6.4.2", + "@fortawesome/vue-fontawesome": "3.0.3", + "@types/bootstrap": "~5.2.8", + "@types/command-exists": "~1.2.3", + "@types/express": "~4.17.21", + "@types/jsonwebtoken": "~9.0.4", + "@typescript-eslint/eslint-plugin": "~6.8.0", + "@typescript-eslint/parser": "~6.8.0", + "@vitejs/plugin-vue": "~4.3.4", + "bootstrap": "5.3.2", + "bootstrap-vue-next": "~0.14.10", + "cross-env": "~7.0.3", + "eslint": "~8.50.0", + "eslint-plugin-jsdoc": "~46.8.2", + "eslint-plugin-vue": "~9.17.0", + "prismjs": "~1.29.0", + "sass": "~1.68.0", + "typescript": "~5.2.2", + "unplugin-vue-components": "~0.25.2", + "vite": "~4.5.0", + "vite-plugin-compression": "~0.5.1", + "vue": "~3.3.8", + "vue-eslint-parser": "~9.3.2", + "vue-i18n": "~9.5.0", + "vue-prism-editor": "2.0.0-alpha.2", + "vue-qrcode": "~2.2.0", + "vue-router": "~4.2.5", + "vue-toastification": "2.0.0-rc.5", + "xterm": "~5.4.0-beta.37", + "xterm-addon-web-links": "~0.9.0" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..f7e7d75 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,4477 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + '@homebridge/node-pty-prebuilt-multiarch': + specifier: ~0.11.10 + version: 0.11.10 + '@louislam/sqlite3': + specifier: ~15.1.6 + version: 15.1.6 + bcryptjs: + specifier: ~2.4.3 + version: 2.4.3 + check-password-strength: + specifier: ~2.0.7 + version: 2.0.7 + command-exists: + specifier: ~1.2.9 + version: 1.2.9 + compare-versions: + specifier: ~6.1.0 + version: 6.1.0 + composerize: + specifier: ~1.4.1 + version: 1.4.1 + croner: + specifier: ~7.0.4 + version: 7.0.4 + dayjs: + specifier: ~1.11.10 + version: 1.11.10 + express: + specifier: ~4.18.2 + version: 4.18.2 + express-static-gzip: + specifier: ~2.1.7 + version: 2.1.7 + http-graceful-shutdown: + specifier: ~3.1.13 + version: 3.1.13 + jsonwebtoken: + specifier: ~9.0.2 + version: 9.0.2 + jwt-decode: + specifier: ~3.1.2 + version: 3.1.2 + knex: + specifier: ~2.5.1 + version: 2.5.1(mysql2@3.6.3) + limiter-es6-compat: + specifier: ~2.1.2 + version: 2.1.2 + mysql2: + specifier: ^3.6.3 + version: 3.6.3 + redbean-node: + specifier: 0.3.2 + version: 0.3.2(mysql2@3.6.3) + socket.io: + specifier: ~4.7.2 + version: 4.7.2 + socket.io-client: + specifier: ~4.7.2 + version: 4.7.2 + timezones-list: + specifier: ~3.0.2 + version: 3.0.2 + ts-command-line-args: + specifier: ~2.5.1 + version: 2.5.1 + tsx: + specifier: ~3.14.0 + version: 3.14.0 + type-fest: + specifier: ~4.3.3 + version: 4.3.3 + yaml: + specifier: ~2.3.4 + version: 2.3.4 + +devDependencies: + '@fortawesome/fontawesome-svg-core': + specifier: 6.4.2 + version: 6.4.2 + '@fortawesome/free-regular-svg-icons': + specifier: 6.4.2 + version: 6.4.2 + '@fortawesome/free-solid-svg-icons': + specifier: 6.4.2 + version: 6.4.2 + '@fortawesome/vue-fontawesome': + specifier: 3.0.3 + version: 3.0.3(@fortawesome/fontawesome-svg-core@6.4.2)(vue@3.3.8) + '@types/bootstrap': + specifier: ~5.2.8 + version: 5.2.8 + '@types/command-exists': + specifier: ~1.2.3 + version: 1.2.3 + '@types/express': + specifier: ~4.17.21 + version: 4.17.21 + '@types/jsonwebtoken': + specifier: ~9.0.4 + version: 9.0.4 + '@typescript-eslint/eslint-plugin': + specifier: ~6.8.0 + version: 6.8.0(@typescript-eslint/parser@6.8.0)(eslint@8.50.0)(typescript@5.2.2) + '@typescript-eslint/parser': + specifier: ~6.8.0 + version: 6.8.0(eslint@8.50.0)(typescript@5.2.2) + '@vitejs/plugin-vue': + specifier: ~4.3.4 + version: 4.3.4(vite@4.5.0)(vue@3.3.8) + bootstrap: + specifier: 5.3.2 + version: 5.3.2(@popperjs/core@2.11.8) + bootstrap-vue-next: + specifier: ~0.14.10 + version: 0.14.10(vue@3.3.8) + cross-env: + specifier: ~7.0.3 + version: 7.0.3 + eslint: + specifier: ~8.50.0 + version: 8.50.0 + eslint-plugin-jsdoc: + specifier: ~46.8.2 + version: 46.8.2(eslint@8.50.0) + eslint-plugin-vue: + specifier: ~9.17.0 + version: 9.17.0(eslint@8.50.0) + prismjs: + specifier: ~1.29.0 + version: 1.29.0 + sass: + specifier: ~1.68.0 + version: 1.68.0 + typescript: + specifier: ~5.2.2 + version: 5.2.2 + unplugin-vue-components: + specifier: ~0.25.2 + version: 0.25.2(vue@3.3.8) + vite: + specifier: ~4.5.0 + version: 4.5.0(sass@1.68.0) + vite-plugin-compression: + specifier: ~0.5.1 + version: 0.5.1(vite@4.5.0) + vue: + specifier: ~3.3.8 + version: 3.3.8(typescript@5.2.2) + vue-eslint-parser: + specifier: ~9.3.2 + version: 9.3.2(eslint@8.50.0) + vue-i18n: + specifier: ~9.5.0 + version: 9.5.0(vue@3.3.8) + vue-prism-editor: + specifier: 2.0.0-alpha.2 + version: 2.0.0-alpha.2(vue@3.3.8) + vue-qrcode: + specifier: ~2.2.0 + version: 2.2.0(qrcode@1.5.3)(vue@3.3.8) + vue-router: + specifier: ~4.2.5 + version: 4.2.5(vue@3.3.8) + vue-toastification: + specifier: 2.0.0-rc.5 + version: 2.0.0-rc.5(vue@3.3.8) + xterm: + specifier: ~5.4.0-beta.37 + version: 5.4.0-beta.37 + xterm-addon-web-links: + specifier: ~0.9.0 + version: 0.9.0(xterm@5.4.0-beta.37) + +packages: + + /@aashutoshrathi/word-wrap@1.2.6: + resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} + engines: {node: '>=0.10.0'} + dev: true + + /@antfu/utils@0.7.6: + resolution: {integrity: sha512-pvFiLP2BeOKA/ZOS6jxx4XhKzdVLHDhGlFEaZ2flWWYf2xOqVniqpk38I04DFRyz+L0ASggl7SkItTc+ZLju4w==} + dev: true + + /@babel/helper-string-parser@7.22.5: + resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-identifier@7.22.20: + resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/parser@7.23.0: + resolution: {integrity: sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.23.0 + dev: true + + /@babel/types@7.23.0: + resolution: {integrity: sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.22.5 + '@babel/helper-validator-identifier': 7.22.20 + to-fast-properties: 2.0.0 + dev: true + + /@es-joy/jsdoccomment@0.40.1: + resolution: {integrity: sha512-YORCdZSusAlBrFpZ77pJjc5r1bQs5caPWtAu+WWmiSo+8XaUzseapVrfAtiRFbQWnrBxxLLEwF6f6ZG/UgCQCg==} + engines: {node: '>=16'} + dependencies: + comment-parser: 1.4.0 + esquery: 1.5.0 + jsdoc-type-pratt-parser: 4.0.0 + dev: true + + /@esbuild/android-arm64@0.18.20: + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + optional: true + + /@esbuild/android-arm@0.18.20: + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + optional: true + + /@esbuild/android-x64@0.18.20: + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + optional: true + + /@esbuild/darwin-arm64@0.18.20: + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + optional: true + + /@esbuild/darwin-x64@0.18.20: + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + optional: true + + /@esbuild/freebsd-arm64@0.18.20: + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + optional: true + + /@esbuild/freebsd-x64@0.18.20: + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + optional: true + + /@esbuild/linux-arm64@0.18.20: + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-arm@0.18.20: + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-ia32@0.18.20: + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-loong64@0.18.20: + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-mips64el@0.18.20: + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-ppc64@0.18.20: + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-riscv64@0.18.20: + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-s390x@0.18.20: + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-x64@0.18.20: + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/netbsd-x64@0.18.20: + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + optional: true + + /@esbuild/openbsd-x64@0.18.20: + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + optional: true + + /@esbuild/sunos-x64@0.18.20: + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + optional: true + + /@esbuild/win32-arm64@0.18.20: + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + optional: true + + /@esbuild/win32-ia32@0.18.20: + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + optional: true + + /@esbuild/win32-x64@0.18.20: + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + optional: true + + /@eslint-community/eslint-utils@4.4.0(eslint@8.50.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.50.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@eslint-community/regexpp@4.10.0: + resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dev: true + + /@eslint/eslintrc@2.1.3: + resolution: {integrity: sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.4 + espree: 9.6.1 + globals: 13.23.0 + ignore: 5.2.4 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@eslint/js@8.50.0: + resolution: {integrity: sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@floating-ui/core@1.5.0: + resolution: {integrity: sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==} + dependencies: + '@floating-ui/utils': 0.1.6 + dev: true + + /@floating-ui/dom@1.5.3: + resolution: {integrity: sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==} + dependencies: + '@floating-ui/core': 1.5.0 + '@floating-ui/utils': 0.1.6 + dev: true + + /@floating-ui/utils@0.1.6: + resolution: {integrity: sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==} + dev: true + + /@floating-ui/vue@1.0.2(vue@3.3.8): + resolution: {integrity: sha512-sImlAl9mAoCKZLNlwWz2P2ZMJIDlOEDXrRD6aD2sIHAka1LPC+nWtB+D3lPe7IE7FGWSbwBPTnlSdlABa3Fr0A==} + dependencies: + '@floating-ui/dom': 1.5.3 + vue-demi: 0.14.6(vue@3.3.8) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + dev: true + + /@fortawesome/fontawesome-common-types@6.4.2: + resolution: {integrity: sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA==} + engines: {node: '>=6'} + requiresBuild: true + dev: true + + /@fortawesome/fontawesome-svg-core@6.4.2: + resolution: {integrity: sha512-gjYDSKv3TrM2sLTOKBc5rH9ckje8Wrwgx1CxAPbN5N3Fm4prfi7NsJVWd1jklp7i5uSCVwhZS5qlhMXqLrpAIg==} + engines: {node: '>=6'} + requiresBuild: true + dependencies: + '@fortawesome/fontawesome-common-types': 6.4.2 + dev: true + + /@fortawesome/free-regular-svg-icons@6.4.2: + resolution: {integrity: sha512-0+sIUWnkgTVVXVAPQmW4vxb9ZTHv0WstOa3rBx9iPxrrrDH6bNLsDYuwXF9b6fGm+iR7DKQvQshUH/FJm3ed9Q==} + engines: {node: '>=6'} + requiresBuild: true + dependencies: + '@fortawesome/fontawesome-common-types': 6.4.2 + dev: true + + /@fortawesome/free-solid-svg-icons@6.4.2: + resolution: {integrity: sha512-sYwXurXUEQS32fZz9hVCUUv/xu49PEJEyUOsA51l6PU/qVgfbTb2glsTEaJngVVT8VqBATRIdh7XVgV1JF1LkA==} + engines: {node: '>=6'} + requiresBuild: true + dependencies: + '@fortawesome/fontawesome-common-types': 6.4.2 + dev: true + + /@fortawesome/vue-fontawesome@3.0.3(@fortawesome/fontawesome-svg-core@6.4.2)(vue@3.3.8): + resolution: {integrity: sha512-KCPHi9QemVXGMrfuwf3nNnNo129resAIQWut9QTAMXmXqL2ErABC6ohd2yY5Ipq0CLWNbKHk8TMdTXL/Zf3ZhA==} + peerDependencies: + '@fortawesome/fontawesome-svg-core': ~1 || ~6 + vue: '>= 3.0.0 < 4' + dependencies: + '@fortawesome/fontawesome-svg-core': 6.4.2 + vue: 3.3.8(typescript@5.2.2) + dev: true + + /@gar/promisify@1.1.3: + resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} + requiresBuild: true + dev: false + optional: true + + /@homebridge/node-pty-prebuilt-multiarch@0.11.10: + resolution: {integrity: sha512-ttOE8QQRq/aRXDoKD2rfYEF50AiDLM9LPTohqCog1Z78g8k3Zqk15R/EHfTl/8cfw4l0fxt3y0dWL56wq79p2A==} + requiresBuild: true + dependencies: + nan: 2.18.0 + prebuild-install: 7.1.1 + dev: false + + /@humanwhocodes/config-array@0.11.13: + resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 2.0.1 + debug: 4.3.4 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@humanwhocodes/module-importer@1.0.1: + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + dev: true + + /@humanwhocodes/object-schema@2.0.1: + resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==} + dev: true + + /@intlify/core-base@9.5.0: + resolution: {integrity: sha512-y3ufM1RJbI/DSmJf3lYs9ACq3S/iRvaSsE3rPIk0MGH7fp+JxU6rdryv/EYcwfcr3Y1aHFlCBir6S391hRZ57w==} + engines: {node: '>= 16'} + dependencies: + '@intlify/message-compiler': 9.5.0 + '@intlify/shared': 9.5.0 + dev: true + + /@intlify/message-compiler@9.5.0: + resolution: {integrity: sha512-CAhVNfEZcOVFg0/5MNyt+OFjvs4J/ARjCj2b+54/FvFP0EDJI5lIqMTSDBE7k0atMROSP0SvWCkwu/AZ5xkK1g==} + engines: {node: '>= 16'} + dependencies: + '@intlify/shared': 9.5.0 + source-map-js: 1.0.2 + dev: true + + /@intlify/shared@9.5.0: + resolution: {integrity: sha512-tAxV14LMXZDZbu32XzLMTsowNlgJNmLwWHYzvMUl6L8gvQeoYiZONjY7AUsqZW8TOZDX9lfvF6adPkk9FSRdDA==} + engines: {node: '>= 16'} + dev: true + + /@isaacs/cliui@8.0.2: + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + string-width-cjs: /string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: /strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: /wrap-ansi@7.0.0 + dev: false + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: true + + /@louislam/sqlite3@15.1.6: + resolution: {integrity: sha512-cVf7hcMrfywYnycatLvorngTFpL3BSWvEy7/NrEfcTyQX8xxj9fdeD553oCTv5fIAk85fluo6mzPq89V3YzrVA==} + requiresBuild: true + peerDependenciesMeta: + node-gyp: + optional: true + dependencies: + '@mapbox/node-pre-gyp': 1.0.11 + node-addon-api: 4.3.0 + tar: 6.2.0 + optionalDependencies: + node-gyp: 8.4.1 + transitivePeerDependencies: + - bluebird + - encoding + - supports-color + dev: false + + /@mapbox/node-pre-gyp@1.0.11: + resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} + hasBin: true + dependencies: + detect-libc: 2.0.2 + https-proxy-agent: 5.0.1 + make-dir: 3.1.0 + node-fetch: 2.7.0 + nopt: 5.0.0 + npmlog: 5.0.1 + rimraf: 3.0.2 + semver: 7.5.4 + tar: 6.2.0 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.15.0 + dev: true + + /@npmcli/fs@1.1.1: + resolution: {integrity: sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==} + requiresBuild: true + dependencies: + '@gar/promisify': 1.1.3 + semver: 7.5.4 + dev: false + optional: true + + /@npmcli/move-file@1.1.2: + resolution: {integrity: sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==} + engines: {node: '>=10'} + deprecated: This functionality has been moved to @npmcli/fs + requiresBuild: true + dependencies: + mkdirp: 1.0.4 + rimraf: 3.0.2 + dev: false + optional: true + + /@pkgjs/parseargs@0.11.0: + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + requiresBuild: true + dev: false + optional: true + + /@popperjs/core@2.11.8: + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + dev: true + + /@rollup/pluginutils@5.0.5: + resolution: {integrity: sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@types/estree': 1.0.5 + estree-walker: 2.0.2 + picomatch: 2.3.1 + dev: true + + /@socket.io/component-emitter@3.1.0: + resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==} + dev: false + + /@tootallnate/once@1.1.2: + resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} + engines: {node: '>= 6'} + requiresBuild: true + dev: false + optional: true + + /@types/body-parser@1.19.5: + resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + dependencies: + '@types/connect': 3.4.38 + '@types/node': 20.8.10 + dev: true + + /@types/bootstrap@5.2.8: + resolution: {integrity: sha512-14do+aWZPc1w3G+YevSsy8eas1XEPhTOUNBhQX/r12YKn7ySssATJusBQ/HCQAd2nq54U8vvrftHSb1YpeJUXg==} + dependencies: + '@popperjs/core': 2.11.8 + dev: true + + /@types/command-exists@1.2.3: + resolution: {integrity: sha512-PpbaE2XWLaWYboXD6k70TcXO/OdOyyRFq5TVpmlUELNxdkkmXU9fkImNosmXU1DtsNrqdUgWd/nJQYXgwmtdXQ==} + dev: true + + /@types/connect@3.4.38: + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + dependencies: + '@types/node': 20.8.10 + dev: true + + /@types/cookie@0.4.1: + resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} + dev: false + + /@types/cors@2.8.16: + resolution: {integrity: sha512-Trx5or1Nyg1Fq138PCuWqoApzvoSLWzZ25ORBiHMbbUT42g578lH1GT4TwYDbiUOLFuDsCkfLneT2105fsFWGg==} + dependencies: + '@types/node': 20.8.10 + dev: false + + /@types/estree@1.0.5: + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + dev: true + + /@types/express-serve-static-core@4.17.41: + resolution: {integrity: sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==} + dependencies: + '@types/node': 20.8.10 + '@types/qs': 6.9.9 + '@types/range-parser': 1.2.6 + '@types/send': 0.17.3 + dev: true + + /@types/express@4.17.21: + resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} + dependencies: + '@types/body-parser': 1.19.5 + '@types/express-serve-static-core': 4.17.41 + '@types/qs': 6.9.9 + '@types/serve-static': 1.15.4 + dev: true + + /@types/http-errors@2.0.3: + resolution: {integrity: sha512-pP0P/9BnCj1OVvQR2lF41EkDG/lWWnDyA203b/4Fmi2eTyORnBtcDoKDwjWQthELrBvWkMOrvSOnZ8OVlW6tXA==} + dev: true + + /@types/json-schema@7.0.14: + resolution: {integrity: sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==} + dev: true + + /@types/jsonwebtoken@9.0.4: + resolution: {integrity: sha512-8UYapdmR0QlxgvJmyE8lP7guxD0UGVMfknsdtCFZh4ovShdBl3iOI4zdvqBHrB/IS+xUj3PSx73Qkey1fhWz+g==} + dependencies: + '@types/node': 20.8.10 + dev: true + + /@types/mime@1.3.4: + resolution: {integrity: sha512-1Gjee59G25MrQGk8bsNvC6fxNiRgUlGn2wlhGf95a59DrprnnHk80FIMMFG9XHMdrfsuA119ht06QPDXA1Z7tw==} + dev: true + + /@types/mime@3.0.3: + resolution: {integrity: sha512-i8MBln35l856k5iOhKk2XJ4SeAWg75mLIpZB4v6imOagKL6twsukBZGDMNhdOVk7yRFTMPpfILocMos59Q1otQ==} + dev: true + + /@types/node@20.3.3: + resolution: {integrity: sha512-wheIYdr4NYML61AjC8MKj/2jrR/kDQri/CIpVoZwldwhnIrD/j9jIU5bJ8yBKuB2VhpFV7Ab6G2XkBjv9r9Zzw==} + dev: false + + /@types/node@20.8.10: + resolution: {integrity: sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==} + dependencies: + undici-types: 5.26.5 + + /@types/qs@6.9.9: + resolution: {integrity: sha512-wYLxw35euwqGvTDx6zfY1vokBFnsK0HNrzc6xNHchxfO2hpuRg74GbkEW7e3sSmPvj0TjCDT1VCa6OtHXnubsg==} + dev: true + + /@types/range-parser@1.2.6: + resolution: {integrity: sha512-+0autS93xyXizIYiyL02FCY8N+KkKPhILhcUSA276HxzreZ16kl+cmwvV2qAM/PuCCwPXzOXOWhiPcw20uSFcA==} + dev: true + + /@types/semver@7.5.4: + resolution: {integrity: sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==} + dev: true + + /@types/send@0.17.3: + resolution: {integrity: sha512-/7fKxvKUoETxjFUsuFlPB9YndePpxxRAOfGC/yJdc9kTjTeP5kRCTzfnE8kPUKCeyiyIZu0YQ76s50hCedI1ug==} + dependencies: + '@types/mime': 1.3.4 + '@types/node': 20.8.10 + dev: true + + /@types/serve-static@1.15.4: + resolution: {integrity: sha512-aqqNfs1XTF0HDrFdlY//+SGUxmdSUbjeRXb5iaZc3x0/vMbYmdw9qvOgHWOyyLFxSSRnUuP5+724zBgfw8/WAw==} + dependencies: + '@types/http-errors': 2.0.3 + '@types/mime': 3.0.3 + '@types/node': 20.8.10 + dev: true + + /@types/web-bluetooth@0.0.18: + resolution: {integrity: sha512-v/ZHEj9xh82usl8LMR3GarzFY1IrbXJw5L4QfQhokjRV91q+SelFqxQWSep1ucXEZ22+dSTwLFkXeur25sPIbw==} + dev: true + + /@typescript-eslint/eslint-plugin@6.8.0(@typescript-eslint/parser@6.8.0)(eslint@8.50.0)(typescript@5.2.2): + resolution: {integrity: sha512-GosF4238Tkes2SHPQ1i8f6rMtG6zlKwMEB0abqSJ3Npvos+doIlc/ATG+vX1G9coDF3Ex78zM3heXHLyWEwLUw==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.10.0 + '@typescript-eslint/parser': 6.8.0(eslint@8.50.0)(typescript@5.2.2) + '@typescript-eslint/scope-manager': 6.8.0 + '@typescript-eslint/type-utils': 6.8.0(eslint@8.50.0)(typescript@5.2.2) + '@typescript-eslint/utils': 6.8.0(eslint@8.50.0)(typescript@5.2.2) + '@typescript-eslint/visitor-keys': 6.8.0 + debug: 4.3.4 + eslint: 8.50.0 + graphemer: 1.4.0 + ignore: 5.2.4 + natural-compare: 1.4.0 + semver: 7.5.4 + ts-api-utils: 1.0.3(typescript@5.2.2) + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser@6.8.0(eslint@8.50.0)(typescript@5.2.2): + resolution: {integrity: sha512-5tNs6Bw0j6BdWuP8Fx+VH4G9fEPDxnVI7yH1IAPkQH5RUtvKwRoqdecAPdQXv4rSOADAaz1LFBZvZG7VbXivSg==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 6.8.0 + '@typescript-eslint/types': 6.8.0 + '@typescript-eslint/typescript-estree': 6.8.0(typescript@5.2.2) + '@typescript-eslint/visitor-keys': 6.8.0 + debug: 4.3.4 + eslint: 8.50.0 + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/scope-manager@6.8.0: + resolution: {integrity: sha512-xe0HNBVwCph7rak+ZHcFD6A+q50SMsFwcmfdjs9Kz4qDh5hWhaPhFjRs/SODEhroBI5Ruyvyz9LfwUJ624O40g==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.8.0 + '@typescript-eslint/visitor-keys': 6.8.0 + dev: true + + /@typescript-eslint/type-utils@6.8.0(eslint@8.50.0)(typescript@5.2.2): + resolution: {integrity: sha512-RYOJdlkTJIXW7GSldUIHqc/Hkto8E+fZN96dMIFhuTJcQwdRoGN2rEWA8U6oXbLo0qufH7NPElUb+MceHtz54g==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 6.8.0(typescript@5.2.2) + '@typescript-eslint/utils': 6.8.0(eslint@8.50.0)(typescript@5.2.2) + debug: 4.3.4 + eslint: 8.50.0 + ts-api-utils: 1.0.3(typescript@5.2.2) + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/types@6.8.0: + resolution: {integrity: sha512-p5qOxSum7W3k+llc7owEStXlGmSl8FcGvhYt8Vjy7FqEnmkCVlM3P57XQEGj58oqaBWDQXbJDZxwUWMS/EAPNQ==} + engines: {node: ^16.0.0 || >=18.0.0} + dev: true + + /@typescript-eslint/typescript-estree@6.8.0(typescript@5.2.2): + resolution: {integrity: sha512-ISgV0lQ8XgW+mvv5My/+iTUdRmGspducmQcDw5JxznasXNnZn3SKNrTRuMsEXv+V/O+Lw9AGcQCfVaOPCAk/Zg==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 6.8.0 + '@typescript-eslint/visitor-keys': 6.8.0 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.4 + ts-api-utils: 1.0.3(typescript@5.2.2) + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/utils@6.8.0(eslint@8.50.0)(typescript@5.2.2): + resolution: {integrity: sha512-dKs1itdE2qFG4jr0dlYLQVppqTE+Itt7GmIf/vX6CSvsW+3ov8PbWauVKyyfNngokhIO9sKZeRGCUo1+N7U98Q==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.50.0) + '@types/json-schema': 7.0.14 + '@types/semver': 7.5.4 + '@typescript-eslint/scope-manager': 6.8.0 + '@typescript-eslint/types': 6.8.0 + '@typescript-eslint/typescript-estree': 6.8.0(typescript@5.2.2) + eslint: 8.50.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/visitor-keys@6.8.0: + resolution: {integrity: sha512-oqAnbA7c+pgOhW2OhGvxm0t1BULX5peQI/rLsNDpGM78EebV3C9IGbX5HNZabuZ6UQrYveCLjKo8Iy/lLlBkkg==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.8.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@vitejs/plugin-vue@4.3.4(vite@4.5.0)(vue@3.3.8): + resolution: {integrity: sha512-ciXNIHKPriERBisHFBvnTbfKa6r9SAesOYXeGDzgegcvy9Q4xdScSHAmKbNT0M3O0S9LKhIf5/G+UYG4NnnzYw==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.0.0 + vue: ^3.2.25 + dependencies: + vite: 4.5.0(sass@1.68.0) + vue: 3.3.8(typescript@5.2.2) + dev: true + + /@vue/compiler-core@3.3.8: + resolution: {integrity: sha512-hN/NNBUECw8SusQvDSqqcVv6gWq8L6iAktUR0UF3vGu2OhzRqcOiAno0FmBJWwxhYEXRlQJT5XnoKsVq1WZx4g==} + dependencies: + '@babel/parser': 7.23.0 + '@vue/shared': 3.3.8 + estree-walker: 2.0.2 + source-map-js: 1.0.2 + dev: true + + /@vue/compiler-dom@3.3.8: + resolution: {integrity: sha512-+PPtv+p/nWDd0AvJu3w8HS0RIm/C6VGBIRe24b9hSyNWOAPEUosFZ5diwawwP8ip5sJ8n0Pe87TNNNHnvjs0FQ==} + dependencies: + '@vue/compiler-core': 3.3.8 + '@vue/shared': 3.3.8 + dev: true + + /@vue/compiler-sfc@3.3.8: + resolution: {integrity: sha512-WMzbUrlTjfYF8joyT84HfwwXo+8WPALuPxhy+BZ6R4Aafls+jDBnSz8PDz60uFhuqFbl3HxRfxvDzrUf3THwpA==} + dependencies: + '@babel/parser': 7.23.0 + '@vue/compiler-core': 3.3.8 + '@vue/compiler-dom': 3.3.8 + '@vue/compiler-ssr': 3.3.8 + '@vue/reactivity-transform': 3.3.8 + '@vue/shared': 3.3.8 + estree-walker: 2.0.2 + magic-string: 0.30.5 + postcss: 8.4.31 + source-map-js: 1.0.2 + dev: true + + /@vue/compiler-ssr@3.3.8: + resolution: {integrity: sha512-hXCqQL/15kMVDBuoBYpUnSYT8doDNwsjvm3jTefnXr+ytn294ySnT8NlsFHmTgKNjwpuFy7XVV8yTeLtNl/P6w==} + dependencies: + '@vue/compiler-dom': 3.3.8 + '@vue/shared': 3.3.8 + dev: true + + /@vue/devtools-api@6.5.1: + resolution: {integrity: sha512-+KpckaAQyfbvshdDW5xQylLni1asvNSGme1JFs8I1+/H5pHEhqUKMEQD/qn3Nx5+/nycBq11qAEi8lk+LXI2dA==} + dev: true + + /@vue/reactivity-transform@3.3.8: + resolution: {integrity: sha512-49CvBzmZNtcHua0XJ7GdGifM8GOXoUMOX4dD40Y5DxI3R8OUhMlvf2nvgUAcPxaXiV5MQQ1Nwy09ADpnLQUqRw==} + dependencies: + '@babel/parser': 7.23.0 + '@vue/compiler-core': 3.3.8 + '@vue/shared': 3.3.8 + estree-walker: 2.0.2 + magic-string: 0.30.5 + dev: true + + /@vue/reactivity@3.3.8: + resolution: {integrity: sha512-ctLWitmFBu6mtddPyOKpHg8+5ahouoTCRtmAHZAXmolDtuZXfjL2T3OJ6DL6ezBPQB1SmMnpzjiWjCiMYmpIuw==} + dependencies: + '@vue/shared': 3.3.8 + dev: true + + /@vue/runtime-core@3.3.8: + resolution: {integrity: sha512-qurzOlb6q26KWQ/8IShHkMDOuJkQnQcTIp1sdP4I9MbCf9FJeGVRXJFr2mF+6bXh/3Zjr9TDgURXrsCr9bfjUw==} + dependencies: + '@vue/reactivity': 3.3.8 + '@vue/shared': 3.3.8 + dev: true + + /@vue/runtime-dom@3.3.8: + resolution: {integrity: sha512-Noy5yM5UIf9UeFoowBVgghyGGPIDPy1Qlqt0yVsUdAVbqI8eeMSsTqBtauaEoT2UFXUk5S64aWVNJN4MJ2vRdA==} + dependencies: + '@vue/runtime-core': 3.3.8 + '@vue/shared': 3.3.8 + csstype: 3.1.2 + dev: true + + /@vue/server-renderer@3.3.8(vue@3.3.8): + resolution: {integrity: sha512-zVCUw7RFskvPuNlPn/8xISbrf0zTWsTSdYTsUTN1ERGGZGVnRxM2QZ3x1OR32+vwkkCm0IW6HmJ49IsPm7ilLg==} + peerDependencies: + vue: 3.3.8 + dependencies: + '@vue/compiler-ssr': 3.3.8 + '@vue/shared': 3.3.8 + vue: 3.3.8(typescript@5.2.2) + dev: true + + /@vue/shared@3.3.8: + resolution: {integrity: sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==} + dev: true + + /@vueuse/core@10.5.0(vue@3.3.8): + resolution: {integrity: sha512-z/tI2eSvxwLRjOhDm0h/SXAjNm8N5ld6/SC/JQs6o6kpJ6Ya50LnEL8g5hoYu005i28L0zqB5L5yAl8Jl26K3A==} + dependencies: + '@types/web-bluetooth': 0.0.18 + '@vueuse/metadata': 10.5.0 + '@vueuse/shared': 10.5.0(vue@3.3.8) + vue-demi: 0.14.6(vue@3.3.8) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + dev: true + + /@vueuse/metadata@10.5.0: + resolution: {integrity: sha512-fEbElR+MaIYyCkeM0SzWkdoMtOpIwO72x8WsZHRE7IggiOlILttqttM69AS13nrDxosnDBYdyy3C5mR1LCxHsw==} + dev: true + + /@vueuse/shared@10.5.0(vue@3.3.8): + resolution: {integrity: sha512-18iyxbbHYLst9MqU1X1QNdMHIjks6wC7XTVf0KNOv5es/Ms6gjVFCAAWTVP2JStuGqydg3DT+ExpFORUEi9yhg==} + dependencies: + vue-demi: 0.14.6(vue@3.3.8) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + dev: true + + /abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + requiresBuild: true + dev: false + + /accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + dev: false + + /acorn-jsx@5.3.2(acorn@8.11.2): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.11.2 + dev: true + + /acorn@8.11.2: + resolution: {integrity: sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + + /agentkeepalive@4.5.0: + resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==} + engines: {node: '>= 8.0.0'} + requiresBuild: true + dependencies: + humanize-ms: 1.2.1 + dev: false + optional: true + + /aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + requiresBuild: true + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + dev: false + optional: true + + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + /ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + dev: false + + /ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + dev: false + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + + /ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + dev: false + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /aproba@2.0.0: + resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} + dev: false + + /are-docs-informative@0.0.2: + resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==} + engines: {node: '>=14'} + dev: true + + /are-we-there-yet@2.0.0: + resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} + engines: {node: '>=10'} + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + dev: false + + /are-we-there-yet@3.0.1: + resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + requiresBuild: true + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + dev: false + optional: true + + /argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + requiresBuild: true + dependencies: + sprintf-js: 1.0.3 + dev: false + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: true + + /array-back@3.1.0: + resolution: {integrity: sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==} + engines: {node: '>=6'} + dev: false + + /array-back@4.0.2: + resolution: {integrity: sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==} + engines: {node: '>=8'} + dev: false + + /array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + dev: false + + /array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + dev: true + + /await-lock@2.2.2: + resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==} + dev: false + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: false + + /base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + dev: false + + /bcryptjs@2.4.3: + resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==} + dev: false + + /binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: true + + /bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: false + + /body-parser@1.20.1: + resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.11.0 + raw-body: 2.5.1 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + dev: true + + /bootstrap-vue-next@0.14.10(vue@3.3.8): + resolution: {integrity: sha512-0czOUVVi8nSA3M9dm48jJnMUB6vP7sYrMxfbFjINE/MEwVbsi4b1Gq41k6gFJcwE54fj7pYQW9q8LP5fqcq0Lw==} + peerDependencies: + vue: ^3.3.4 + dependencies: + '@floating-ui/vue': 1.0.2(vue@3.3.8) + '@vueuse/core': 10.5.0(vue@3.3.8) + vue: 3.3.8(typescript@5.2.2) + transitivePeerDependencies: + - '@vue/composition-api' + dev: true + + /bootstrap@5.3.2(@popperjs/core@2.11.8): + resolution: {integrity: sha512-D32nmNWiQHo94BKHLmOrdjlL05q1c8oxbtBphQFb9Z5to6eGRDCm0QgeaZ4zFBHzfg2++rqa2JkqCcxDy0sH0g==} + peerDependencies: + '@popperjs/core': ^2.11.8 + dependencies: + '@popperjs/core': 2.11.8 + dev: true + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true + + /buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + dev: false + + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: false + + /buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: false + + /builtin-modules@3.3.0: + resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} + engines: {node: '>=6'} + dev: true + + /bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + dev: false + + /cacache@15.3.0: + resolution: {integrity: sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==} + engines: {node: '>= 10'} + requiresBuild: true + dependencies: + '@npmcli/fs': 1.1.1 + '@npmcli/move-file': 1.1.2 + chownr: 2.0.0 + fs-minipass: 2.1.0 + glob: 7.2.3 + infer-owner: 1.0.4 + lru-cache: 6.0.0 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + mkdirp: 1.0.4 + p-map: 4.0.0 + promise-inflight: 1.0.1 + rimraf: 3.0.2 + ssri: 8.0.1 + tar: 6.2.0 + unique-filename: 1.1.1 + transitivePeerDependencies: + - bluebird + dev: false + optional: true + + /call-bind@1.0.5: + resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} + dependencies: + function-bind: 1.1.2 + get-intrinsic: 1.2.2 + set-function-length: 1.1.1 + dev: false + + /callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + dev: true + + /camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + /chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + dev: false + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + /check-password-strength@2.0.7: + resolution: {integrity: sha512-VyklBkB6dOKnCIh63zdVr7QKVMN9/npwUqNAXxWrz8HabVZH/n/d+lyNm1O/vbXFJlT/Hytb5ouYKYGkoeZirQ==} + dev: false + + /chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + dev: false + + /chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + dev: false + + /clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + requiresBuild: true + dev: false + optional: true + + /cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + dev: true + + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + dev: false + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + dev: false + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + /color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + dev: false + + /colorette@2.0.19: + resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==} + dev: false + + /command-exists@1.2.9: + resolution: {integrity: sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==} + dev: false + + /command-line-args@5.2.1: + resolution: {integrity: sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==} + engines: {node: '>=4.0.0'} + dependencies: + array-back: 3.1.0 + find-replace: 3.0.0 + lodash.camelcase: 4.3.0 + typical: 4.0.0 + dev: false + + /command-line-usage@6.1.3: + resolution: {integrity: sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==} + engines: {node: '>=8.0.0'} + dependencies: + array-back: 4.0.2 + chalk: 2.4.2 + table-layout: 1.0.2 + typical: 5.2.0 + dev: false + + /commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + dev: false + + /commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + dev: false + + /comment-parser@1.4.0: + resolution: {integrity: sha512-QLyTNiZ2KDOibvFPlZ6ZngVsZ/0gYnE6uTXi5aoDg8ed3AkJAz4sEje3Y8a29hQ1s6A99MZXe47fLAXQ1rTqaw==} + engines: {node: '>= 12.0.0'} + dev: true + + /compare-versions@6.1.0: + resolution: {integrity: sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg==} + dev: false + + /composerize@1.4.1: + resolution: {integrity: sha512-63bKZMgOE8Fd6jBSmkZFdOtKM5A46TafrWgNHVXIBfTu/YBBZw91xkjALG3JiuOatX4tMK04uM37O4HGypnkfQ==} + hasBin: true + dependencies: + core-js: 2.6.12 + deepmerge: 2.2.1 + invariant: 2.2.4 + yamljs: 0.3.0 + yargs-parser: 13.1.2 + dev: false + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + /console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + dev: false + + /content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + dev: false + + /cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + dev: false + + /cookie@0.4.2: + resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} + engines: {node: '>= 0.6'} + dev: false + + /cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + dev: false + + /core-js@2.6.12: + resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==} + deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. + requiresBuild: true + dev: false + + /cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + dev: false + + /croner@7.0.4: + resolution: {integrity: sha512-P8Zd88km8oQ0xH8Es0u75GtOnFyCNopuAhlFv5kAnbcTuXd0xNvRTgnxnJEs63FicCOsHTL7rpu4BHzY3cMq4w==} + engines: {node: '>=6.0'} + dev: false + + /cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + dependencies: + cross-spawn: 7.0.3 + dev: true + + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + /cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /csstype@3.1.2: + resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} + dev: true + + /dayjs@1.11.10: + resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} + dev: false + + /debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.0.0 + dev: false + + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + + /decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + /decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + dependencies: + mimic-response: 3.1.0 + dev: false + + /deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + dev: false + + /deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: true + + /deepmerge@2.2.1: + resolution: {integrity: sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==} + engines: {node: '>=0.10.0'} + dev: false + + /define-data-property@1.1.1: + resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.2 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + dev: false + + /delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + dev: false + + /denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + dev: false + + /depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dev: false + + /destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dev: false + + /detect-libc@2.0.2: + resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} + engines: {node: '>=8'} + dev: false + + /dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + dev: true + + /dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dependencies: + path-type: 4.0.0 + dev: true + + /doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: false + + /ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + dev: false + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + requiresBuild: true + + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: false + + /encode-utf8@1.0.3: + resolution: {integrity: sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==} + dev: true + + /encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + dev: false + + /encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + requiresBuild: true + dependencies: + iconv-lite: 0.6.3 + dev: false + optional: true + + /end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + dependencies: + once: 1.4.0 + dev: false + + /engine.io-client@6.5.2: + resolution: {integrity: sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg==} + dependencies: + '@socket.io/component-emitter': 3.1.0 + debug: 4.3.4 + engine.io-parser: 5.2.1 + ws: 8.11.0 + xmlhttprequest-ssl: 2.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + + /engine.io-parser@5.2.1: + resolution: {integrity: sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==} + engines: {node: '>=10.0.0'} + dev: false + + /engine.io@6.5.3: + resolution: {integrity: sha512-IML/R4eG/pUS5w7OfcDE0jKrljWS9nwnEfsxWCIJF5eO6AHo6+Hlv+lQbdlAYsiJPHzUthLm1RUjnBzWOs45cw==} + engines: {node: '>=10.2.0'} + dependencies: + '@types/cookie': 0.4.1 + '@types/cors': 2.8.16 + '@types/node': 20.8.10 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.4.2 + cors: 2.8.5 + debug: 4.3.4 + engine.io-parser: 5.2.1 + ws: 8.11.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + + /env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + requiresBuild: true + dev: false + optional: true + + /err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + requiresBuild: true + dev: false + optional: true + + /esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + /escalade@3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + dev: false + + /escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + dev: false + + /escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + dev: false + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: true + + /eslint-plugin-jsdoc@46.8.2(eslint@8.50.0): + resolution: {integrity: sha512-5TSnD018f3tUJNne4s4gDWQflbsgOycIKEUBoCLn6XtBMgNHxQFmV8vVxUtiPxAQq8lrX85OaSG/2gnctxw9uQ==} + engines: {node: '>=16'} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@es-joy/jsdoccomment': 0.40.1 + are-docs-informative: 0.0.2 + comment-parser: 1.4.0 + debug: 4.3.4 + escape-string-regexp: 4.0.0 + eslint: 8.50.0 + esquery: 1.5.0 + is-builtin-module: 3.2.1 + semver: 7.5.4 + spdx-expression-parse: 3.0.1 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-plugin-vue@9.17.0(eslint@8.50.0): + resolution: {integrity: sha512-r7Bp79pxQk9I5XDP0k2dpUC7Ots3OSWgvGZNu3BxmKK6Zg7NgVtcOB6OCna5Kb9oQwJPl5hq183WD0SY5tZtIQ==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.50.0) + eslint: 8.50.0 + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 6.0.13 + semver: 7.5.4 + vue-eslint-parser: 9.3.2(eslint@8.50.0) + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: true + + /eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /eslint@8.50.0: + resolution: {integrity: sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.50.0) + '@eslint-community/regexpp': 4.10.0 + '@eslint/eslintrc': 2.1.3 + '@eslint/js': 8.50.0 + '@humanwhocodes/config-array': 0.11.13 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.23.0 + graphemer: 1.4.0 + ignore: 5.2.4 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.3 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: true + + /esm@3.2.25: + resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} + engines: {node: '>=6'} + dev: false + + /espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.11.2 + acorn-jsx: 5.3.2(acorn@8.11.2) + eslint-visitor-keys: 3.4.3 + dev: true + + /esquery@1.5.0: + resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + dev: true + + /esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + dev: true + + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + dev: true + + /estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + dev: true + + /esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: true + + /etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + dev: false + + /expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + dev: false + + /express-static-gzip@2.1.7: + resolution: {integrity: sha512-QOCZUC+lhPPCjIJKpQGu1Oa61Axg9Mq09Qvit8Of7kzpMuwDeMSqjjQteQS3OVw/GkENBoSBheuQDWPlngImvw==} + dependencies: + serve-static: 1.15.0 + transitivePeerDependencies: + - supports-color + dev: false + + /express@4.18.2: + resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} + engines: {node: '>= 0.10.0'} + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.1 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.5.0 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.2.0 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.1 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.7 + proxy-addr: 2.0.7 + qs: 6.11.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.18.0 + serve-static: 1.15.0 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + dev: false + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: true + + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true + + /fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: true + + /fastq@1.15.0: + resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} + dependencies: + reusify: 1.0.4 + dev: true + + /file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flat-cache: 3.1.1 + dev: true + + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /finalhandler@1.2.0: + resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} + engines: {node: '>= 0.8'} + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /find-replace@3.0.0: + resolution: {integrity: sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==} + engines: {node: '>=4.0.0'} + dependencies: + array-back: 3.1.0 + dev: false + + /find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + dev: true + + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: true + + /flat-cache@3.1.1: + resolution: {integrity: sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==} + engines: {node: '>=12.0.0'} + dependencies: + flatted: 3.2.9 + keyv: 4.5.4 + rimraf: 3.0.2 + dev: true + + /flatted@3.2.9: + resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} + dev: true + + /foreground-child@3.1.1: + resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} + engines: {node: '>=14'} + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + dev: false + + /forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + dev: false + + /fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + dev: false + + /fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + dev: false + + /fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + dev: true + + /fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + dependencies: + minipass: 3.3.6 + dev: false + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + requiresBuild: true + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + optional: true + + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + /gauge@3.0.2: + resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} + engines: {node: '>=10'} + dependencies: + aproba: 2.0.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + object-assign: 4.1.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + dev: false + + /gauge@4.0.4: + resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + requiresBuild: true + dependencies: + aproba: 2.0.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + dev: false + optional: true + + /generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + dependencies: + is-property: 1.0.2 + dev: false + + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + dev: true + + /get-intrinsic@1.2.2: + resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} + dependencies: + function-bind: 1.1.2 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.0 + dev: false + + /get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + dev: false + + /get-tsconfig@4.7.2: + resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} + dependencies: + resolve-pkg-maps: 1.0.0 + dev: false + + /getopts@2.3.0: + resolution: {integrity: sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==} + dev: false + + /github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + dev: false + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob@10.3.10: + resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + foreground-child: 3.1.1 + jackspeak: 2.3.6 + minimatch: 9.0.3 + minipass: 7.0.4 + path-scurry: 1.10.1 + dev: false + + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + requiresBuild: true + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + /globals@13.23.0: + resolution: {integrity: sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: true + + /globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.2 + ignore: 5.2.4 + merge2: 1.4.1 + slash: 3.0.0 + dev: true + + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.2 + dev: false + + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + /graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + dev: true + + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + dev: false + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + /has-property-descriptors@1.0.1: + resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} + dependencies: + get-intrinsic: 1.2.2 + dev: false + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + dev: false + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: false + + /has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + dev: false + + /hasown@2.0.0: + resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + + /http-cache-semantics@4.1.1: + resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + requiresBuild: true + dev: false + optional: true + + /http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + dev: false + + /http-graceful-shutdown@3.1.13: + resolution: {integrity: sha512-Ci5LRufQ8AtrQ1U26AevS8QoMXDOhnAHCJI3eZu1com7mZGHxREmw3dNj85ftpQokQCvak8nI2pnFS8zyM1M+Q==} + engines: {node: '>=4.0.0'} + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + + /http-proxy-agent@4.0.1: + resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==} + engines: {node: '>= 6'} + requiresBuild: true + dependencies: + '@tootallnate/once': 1.1.2 + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + optional: true + + /https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + dependencies: + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + + /humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + requiresBuild: true + dependencies: + ms: 2.1.3 + dev: false + optional: true + + /iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + + /iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + requiresBuild: true + dependencies: + safer-buffer: 2.1.2 + dev: false + + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: false + + /ignore@5.2.4: + resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} + engines: {node: '>= 4'} + dev: true + + /immutable@4.3.4: + resolution: {integrity: sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==} + dev: true + + /import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + dev: true + + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + /indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + requiresBuild: true + dev: false + optional: true + + /infer-owner@1.0.4: + resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} + requiresBuild: true + dev: false + optional: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + requiresBuild: true + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + /ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + requiresBuild: true + dev: false + + /interpret@2.2.0: + resolution: {integrity: sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==} + engines: {node: '>= 0.10'} + dev: false + + /invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + dependencies: + loose-envify: 1.4.0 + dev: false + + /ip@2.0.0: + resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==} + requiresBuild: true + dev: false + optional: true + + /ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + dev: false + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: true + + /is-builtin-module@3.2.1: + resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} + engines: {node: '>=6'} + dependencies: + builtin-modules: 3.3.0 + dev: true + + /is-core-module@2.13.1: + resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + dependencies: + hasown: 2.0.0 + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + requiresBuild: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-lambda@1.0.1: + resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} + requiresBuild: true + dev: false + optional: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + dev: true + + /is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + dev: false + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + requiresBuild: true + + /jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} + engines: {node: '>=14'} + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + dev: false + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: false + + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: true + + /jsdoc-type-pratt-parser@4.0.0: + resolution: {integrity: sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==} + engines: {node: '>=12.0.0'} + dev: true + + /json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + dev: true + + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: true + + /json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + dev: true + + /jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + dev: true + + /jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.5.4 + dev: false + + /just-performance-es6-compat@4.3.2: + resolution: {integrity: sha512-bNPuqRaV8PrqYVivdN8FsKi0rmXaQ0Y63oasFUQFJooJPH0r26LdfBAfoodhUQI6I6cek9yNo+tnGiF0R3pIHw==} + dev: false + + /jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + dev: false + + /jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + dev: false + + /jwt-decode@3.1.2: + resolution: {integrity: sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==} + dev: false + + /keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + dependencies: + json-buffer: 3.0.1 + dev: true + + /knex@2.4.2(mysql2@3.6.3): + resolution: {integrity: sha512-tMI1M7a+xwHhPxjbl/H9K1kHX+VncEYcvCx5K00M16bWvpYPKAZd6QrCu68PtHAdIZNQPWZn0GVhqVBEthGWCg==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + better-sqlite3: '*' + mysql: '*' + mysql2: '*' + pg: '*' + pg-native: '*' + sqlite3: '*' + tedious: '*' + peerDependenciesMeta: + better-sqlite3: + optional: true + mysql: + optional: true + mysql2: + optional: true + pg: + optional: true + pg-native: + optional: true + sqlite3: + optional: true + tedious: + optional: true + dependencies: + colorette: 2.0.19 + commander: 9.5.0 + debug: 4.3.4 + escalade: 3.1.1 + esm: 3.2.25 + get-package-type: 0.1.0 + getopts: 2.3.0 + interpret: 2.2.0 + lodash: 4.17.21 + mysql2: 3.6.3 + pg-connection-string: 2.5.0 + rechoir: 0.8.0 + resolve-from: 5.0.0 + tarn: 3.0.2 + tildify: 2.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /knex@2.5.1(mysql2@3.6.3): + resolution: {integrity: sha512-z78DgGKUr4SE/6cm7ku+jHvFT0X97aERh/f0MUKAKgFnwCYBEW4TFBqtHWFYiJFid7fMrtpZ/gxJthvz5mEByA==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + better-sqlite3: '*' + mysql: '*' + mysql2: '*' + pg: '*' + pg-native: '*' + sqlite3: '*' + tedious: '*' + peerDependenciesMeta: + better-sqlite3: + optional: true + mysql: + optional: true + mysql2: + optional: true + pg: + optional: true + pg-native: + optional: true + sqlite3: + optional: true + tedious: + optional: true + dependencies: + colorette: 2.0.19 + commander: 10.0.1 + debug: 4.3.4 + escalade: 3.1.1 + esm: 3.2.25 + get-package-type: 0.1.0 + getopts: 2.3.0 + interpret: 2.2.0 + lodash: 4.17.21 + mysql2: 3.6.3 + pg-connection-string: 2.6.1 + rechoir: 0.8.0 + resolve-from: 5.0.0 + tarn: 3.0.2 + tildify: 2.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /limiter-es6-compat@2.1.2: + resolution: {integrity: sha512-rAi6LCOubXfnojSePboCylTSJWi9m0hlnZJ6AYsBv0clRFr4PJAZd8DtDVhXSN5Ed5nXa6pULHu9sJxoxpWWww==} + dependencies: + just-performance-es6-compat: 4.3.2 + dev: false + + /local-pkg@0.4.3: + resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==} + engines: {node: '>=14'} + dev: true + + /locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + dependencies: + p-locate: 4.1.0 + dev: true + + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: true + + /lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + dev: false + + /lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + dev: false + + /lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + dev: false + + /lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + dev: false + + /lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + dev: false + + /lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + dev: false + + /lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + dev: false + + /lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + dev: true + + /lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + dev: false + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + /long@5.2.3: + resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} + dev: false + + /loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + dependencies: + js-tokens: 4.0.0 + dev: false + + /lru-cache@10.0.1: + resolution: {integrity: sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==} + engines: {node: 14 || >=16.14} + dev: false + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + + /lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + dev: false + + /lru-cache@8.0.5: + resolution: {integrity: sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==} + engines: {node: '>=16.14'} + dev: false + + /magic-string@0.30.5: + resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + dependencies: + semver: 6.3.1 + dev: false + + /make-fetch-happen@9.1.0: + resolution: {integrity: sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==} + engines: {node: '>= 10'} + requiresBuild: true + dependencies: + agentkeepalive: 4.5.0 + cacache: 15.3.0 + http-cache-semantics: 4.1.1 + http-proxy-agent: 4.0.1 + https-proxy-agent: 5.0.1 + is-lambda: 1.0.1 + lru-cache: 6.0.0 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-fetch: 1.4.1 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + negotiator: 0.6.3 + promise-retry: 2.0.1 + socks-proxy-agent: 6.2.1 + ssri: 8.0.1 + transitivePeerDependencies: + - bluebird + - supports-color + dev: false + optional: true + + /media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + dev: false + + /merge-descriptors@1.0.1: + resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} + dev: false + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + dev: false + + /micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + dev: true + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: false + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: false + + /mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + dev: false + + /mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + dev: false + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + + /minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: false + + /minipass-collect@1.0.2: + resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} + engines: {node: '>= 8'} + requiresBuild: true + dependencies: + minipass: 3.3.6 + dev: false + optional: true + + /minipass-fetch@1.4.1: + resolution: {integrity: sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==} + engines: {node: '>=8'} + requiresBuild: true + dependencies: + minipass: 3.3.6 + minipass-sized: 1.0.3 + minizlib: 2.1.2 + optionalDependencies: + encoding: 0.1.13 + dev: false + optional: true + + /minipass-flush@1.0.5: + resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} + engines: {node: '>= 8'} + requiresBuild: true + dependencies: + minipass: 3.3.6 + dev: false + optional: true + + /minipass-pipeline@1.2.4: + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} + requiresBuild: true + dependencies: + minipass: 3.3.6 + dev: false + optional: true + + /minipass-sized@1.0.3: + resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} + engines: {node: '>=8'} + requiresBuild: true + dependencies: + minipass: 3.3.6 + dev: false + optional: true + + /minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + dependencies: + yallist: 4.0.0 + dev: false + + /minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + dev: false + + /minipass@7.0.4: + resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} + engines: {node: '>=16 || 14 >=14.17'} + dev: false + + /minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + dev: false + + /mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + dev: false + + /mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + dev: false + + /ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + dev: false + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: false + + /mysql2@3.6.3: + resolution: {integrity: sha512-qYd/1CDuW1KYZjD4tzg2O8YS3X/UWuGH8ZMHyMeggMTXL3yOdMisbwZ5SNkHzDGlZXKYLAvV8tMrEH+NUMz3fw==} + engines: {node: '>= 8.0'} + dependencies: + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.6.3 + long: 5.2.3 + lru-cache: 8.0.5 + named-placeholders: 1.1.3 + seq-queue: 0.0.5 + sqlstring: 2.3.3 + dev: false + + /named-placeholders@1.1.3: + resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==} + engines: {node: '>=12.0.0'} + dependencies: + lru-cache: 7.18.3 + dev: false + + /nan@2.18.0: + resolution: {integrity: sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==} + dev: false + + /nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /napi-build-utils@1.0.2: + resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} + dev: false + + /natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + dev: true + + /negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + requiresBuild: true + dev: false + + /node-abi@3.51.0: + resolution: {integrity: sha512-SQkEP4hmNWjlniS5zdnfIXTk1x7Ome85RDzHlTbBtzE97Gfwz/Ipw4v/Ryk20DWIy3yCNVLVlGKApCnmvYoJbA==} + engines: {node: '>=10'} + dependencies: + semver: 7.5.4 + dev: false + + /node-addon-api@4.3.0: + resolution: {integrity: sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==} + dev: false + + /node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: false + + /node-gyp@8.4.1: + resolution: {integrity: sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==} + engines: {node: '>= 10.12.0'} + hasBin: true + requiresBuild: true + dependencies: + env-paths: 2.2.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + make-fetch-happen: 9.1.0 + nopt: 5.0.0 + npmlog: 6.0.2 + rimraf: 3.0.2 + semver: 7.5.4 + tar: 6.2.0 + which: 2.0.2 + transitivePeerDependencies: + - bluebird + - supports-color + dev: false + optional: true + + /nopt@5.0.0: + resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} + engines: {node: '>=6'} + hasBin: true + dependencies: + abbrev: 1.1.1 + dev: false + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /npmlog@5.0.1: + resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} + dependencies: + are-we-there-yet: 2.0.0 + console-control-strings: 1.1.0 + gauge: 3.0.2 + set-blocking: 2.0.0 + dev: false + + /npmlog@6.0.2: + resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + requiresBuild: true + dependencies: + are-we-there-yet: 3.0.1 + console-control-strings: 1.1.0 + gauge: 4.0.4 + set-blocking: 2.0.0 + dev: false + optional: true + + /nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + dependencies: + boolbase: 1.0.0 + dev: true + + /object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + dev: false + + /object-inspect@1.13.1: + resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + dev: false + + /on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + dependencies: + ee-first: 1.1.1 + dev: false + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + requiresBuild: true + dependencies: + wrappy: 1.0.2 + + /optionator@0.9.3: + resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} + engines: {node: '>= 0.8.0'} + dependencies: + '@aashutoshrathi/word-wrap': 1.2.6 + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + dependencies: + p-try: 2.2.0 + dev: true + + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + dev: true + + /p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + dependencies: + p-limit: 2.3.0 + dev: true + + /p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: true + + /p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + requiresBuild: true + dependencies: + aggregate-error: 3.1.0 + dev: false + optional: true + + /p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + dev: true + + /parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + dev: true + + /parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + dev: false + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: true + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + requiresBuild: true + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + /path-scurry@1.10.1: + resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + lru-cache: 10.0.1 + minipass: 7.0.4 + dev: false + + /path-to-regexp@0.1.7: + resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + dev: false + + /path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + dev: true + + /pg-connection-string@2.5.0: + resolution: {integrity: sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==} + dev: false + + /pg-connection-string@2.6.1: + resolution: {integrity: sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==} + dev: false + + /picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + dev: true + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + dev: true + + /postcss-selector-parser@6.0.13: + resolution: {integrity: sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==} + engines: {node: '>=4'} + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + dev: true + + /postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + + /prebuild-install@7.1.1: + resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + detect-libc: 2.0.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 1.0.2 + node-abi: 3.51.0 + pump: 3.0.0 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.1 + tunnel-agent: 0.6.0 + dev: false + + /prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + dev: true + + /prismjs@1.29.0: + resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} + engines: {node: '>=6'} + dev: true + + /promise-inflight@1.0.1: + resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} + requiresBuild: true + peerDependencies: + bluebird: '*' + peerDependenciesMeta: + bluebird: + optional: true + dev: false + optional: true + + /promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + requiresBuild: true + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + dev: false + optional: true + + /proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + dev: false + + /pump@3.0.0: + resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + dev: false + + /punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + dev: true + + /qrcode@1.5.3: + resolution: {integrity: sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==} + engines: {node: '>=10.13.0'} + hasBin: true + dependencies: + dijkstrajs: 1.0.3 + encode-utf8: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 + dev: true + + /qs@6.11.0: + resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.4 + dev: false + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + dev: false + + /raw-body@2.5.1: + resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + dev: false + + /rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + dev: false + + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: false + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /rechoir@0.8.0: + resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} + engines: {node: '>= 10.13.0'} + dependencies: + resolve: 1.22.8 + dev: false + + /redbean-node@0.3.2(mysql2@3.6.3): + resolution: {integrity: sha512-39VMxPWPpPicRlU4FSJJnJuUMoxw5/4envFthHtKnLe+3qWTBje3RMrJTFZcQGLruWQ/s2LgeYzdd+d0O+p+uQ==} + dependencies: + '@types/node': 20.3.3 + await-lock: 2.2.2 + dayjs: 1.11.10 + glob: 10.3.10 + knex: 2.4.2(mysql2@3.6.3) + lodash: 4.17.21 + transitivePeerDependencies: + - better-sqlite3 + - mysql + - mysql2 + - pg + - pg-native + - sqlite3 + - supports-color + - tedious + dev: false + + /reduce-flatten@2.0.0: + resolution: {integrity: sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==} + engines: {node: '>=6'} + dev: false + + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + dev: true + + /require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + dev: true + + /resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + dev: true + + /resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + dev: false + + /resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + dev: false + + /resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + dependencies: + is-core-module: 2.13.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + /retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + requiresBuild: true + dev: false + optional: true + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + hasBin: true + dependencies: + glob: 7.2.3 + + /rollup@3.29.4: + resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: false + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: false + + /sass@1.68.0: + resolution: {integrity: sha512-Lmj9lM/fef0nQswm1J2HJcEsBUba4wgNx2fea6yJHODREoMFnwRpZydBnX/RjyXw2REIwdkbqE4hrTo4qfDBUA==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + chokidar: 3.5.3 + immutable: 4.3.4 + source-map-js: 1.0.2 + dev: true + + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + dev: false + + /semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + + /send@0.18.0: + resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} + engines: {node: '>= 0.8.0'} + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: false + + /seq-queue@0.0.5: + resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + dev: false + + /serve-static@1.15.0: + resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} + engines: {node: '>= 0.8.0'} + dependencies: + encodeurl: 1.0.2 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.18.0 + transitivePeerDependencies: + - supports-color + dev: false + + /set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + /set-function-length@1.1.1: + resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + get-intrinsic: 1.2.2 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + dev: false + + /setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + dev: false + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + /side-channel@1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + object-inspect: 1.13.1 + dev: false + + /signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + dev: false + + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: false + + /simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + dev: false + + /simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + dev: false + + /slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + dev: true + + /smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + requiresBuild: true + dev: false + optional: true + + /socket.io-adapter@2.5.2: + resolution: {integrity: sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==} + dependencies: + ws: 8.11.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + + /socket.io-client@4.7.2: + resolution: {integrity: sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==} + engines: {node: '>=10.0.0'} + dependencies: + '@socket.io/component-emitter': 3.1.0 + debug: 4.3.4 + engine.io-client: 6.5.2 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + + /socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + dependencies: + '@socket.io/component-emitter': 3.1.0 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + + /socket.io@4.7.2: + resolution: {integrity: sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==} + engines: {node: '>=10.2.0'} + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.5 + debug: 4.3.4 + engine.io: 6.5.3 + socket.io-adapter: 2.5.2 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + + /socks-proxy-agent@6.2.1: + resolution: {integrity: sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==} + engines: {node: '>= 10'} + requiresBuild: true + dependencies: + agent-base: 6.0.2 + debug: 4.3.4 + socks: 2.7.1 + transitivePeerDependencies: + - supports-color + dev: false + optional: true + + /socks@2.7.1: + resolution: {integrity: sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==} + engines: {node: '>= 10.13.0', npm: '>= 3.0.0'} + requiresBuild: true + dependencies: + ip: 2.0.0 + smart-buffer: 4.2.0 + dev: false + optional: true + + /source-map-js@1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + dev: true + + /source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: false + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: false + + /spdx-exceptions@2.3.0: + resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==} + dev: true + + /spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + dependencies: + spdx-exceptions: 2.3.0 + spdx-license-ids: 3.0.16 + dev: true + + /spdx-license-ids@3.0.16: + resolution: {integrity: sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==} + dev: true + + /sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + requiresBuild: true + dev: false + + /sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + dev: false + + /ssri@8.0.1: + resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==} + engines: {node: '>= 8'} + requiresBuild: true + dependencies: + minipass: 3.3.6 + dev: false + optional: true + + /statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + dev: false + + /string-format@2.0.0: + resolution: {integrity: sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==} + dev: false + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + /string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + dev: false + + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + requiresBuild: true + dependencies: + safe-buffer: 5.2.1 + dev: false + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + dev: false + + /strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + dev: false + + /strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: true + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: false + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + /table-layout@1.0.2: + resolution: {integrity: sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==} + engines: {node: '>=8.0.0'} + dependencies: + array-back: 4.0.2 + deep-extend: 0.6.0 + typical: 5.2.0 + wordwrapjs: 4.0.1 + dev: false + + /tar-fs@2.1.1: + resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.0 + tar-stream: 2.2.0 + dev: false + + /tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.4 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: false + + /tar@6.2.0: + resolution: {integrity: sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==} + engines: {node: '>=10'} + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + dev: false + + /tarn@3.0.2: + resolution: {integrity: sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==} + engines: {node: '>=8.0.0'} + dev: false + + /text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + dev: true + + /tildify@2.0.0: + resolution: {integrity: sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==} + engines: {node: '>=8'} + dev: false + + /timezones-list@3.0.2: + resolution: {integrity: sha512-I698hm6Jp/xxkwyTSOr39pZkYKETL8LDJeSIhjxXBfPUAHM5oZNuQ4o9UK3PSkDBOkjATecSOBb3pR1IkIBUsg==} + dev: false + + /to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + dev: true + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + dev: false + + /tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: false + + /ts-api-utils@1.0.3(typescript@5.2.2): + resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==} + engines: {node: '>=16.13.0'} + peerDependencies: + typescript: '>=4.2.0' + dependencies: + typescript: 5.2.2 + dev: true + + /ts-command-line-args@2.5.1: + resolution: {integrity: sha512-H69ZwTw3rFHb5WYpQya40YAX2/w7Ut75uUECbgBIsLmM+BNuYnxsltfyyLMxy6sEeKxgijLTnQtLd0nKd6+IYw==} + hasBin: true + dependencies: + chalk: 4.1.2 + command-line-args: 5.2.1 + command-line-usage: 6.1.3 + string-format: 2.0.0 + dev: false + + /tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + requiresBuild: true + dev: true + + /tsx@3.14.0: + resolution: {integrity: sha512-xHtFaKtHxM9LOklMmJdI3BEnQq/D5F73Of2E1GDrITi9sgoVkvIsrQUTY1G8FlmGtA+awCI4EBlTRRYxkL2sRg==} + hasBin: true + dependencies: + esbuild: 0.18.20 + get-tsconfig: 4.7.2 + source-map-support: 0.5.21 + optionalDependencies: + fsevents: 2.3.3 + dev: false + + /tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + dev: true + + /type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + dev: true + + /type-fest@4.3.3: + resolution: {integrity: sha512-bxhiFii6BBv6UiSDq7uKTMyADT9unXEl3ydGefndVLxFeB44LRbT4K7OJGDYSyDrKnklCC1Pre68qT2wbUl2Aw==} + engines: {node: '>=16'} + dev: false + + /type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + dev: false + + /typescript@5.2.2: + resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} + engines: {node: '>=14.17'} + hasBin: true + dev: true + + /typical@4.0.0: + resolution: {integrity: sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==} + engines: {node: '>=8'} + dev: false + + /typical@5.2.0: + resolution: {integrity: sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==} + engines: {node: '>=8'} + dev: false + + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + /unique-filename@1.1.1: + resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==} + requiresBuild: true + dependencies: + unique-slug: 2.0.2 + dev: false + optional: true + + /unique-slug@2.0.2: + resolution: {integrity: sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==} + requiresBuild: true + dependencies: + imurmurhash: 0.1.4 + dev: false + optional: true + + /universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + dev: true + + /unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + dev: false + + /unplugin-vue-components@0.25.2(vue@3.3.8): + resolution: {integrity: sha512-OVmLFqILH6w+eM8fyt/d/eoJT9A6WO51NZLf1vC5c1FZ4rmq2bbGxTy8WP2Jm7xwFdukaIdv819+UI7RClPyCA==} + engines: {node: '>=14'} + peerDependencies: + '@babel/parser': ^7.15.8 + '@nuxt/kit': ^3.2.2 + vue: 2 || 3 + peerDependenciesMeta: + '@babel/parser': + optional: true + '@nuxt/kit': + optional: true + dependencies: + '@antfu/utils': 0.7.6 + '@rollup/pluginutils': 5.0.5 + chokidar: 3.5.3 + debug: 4.3.4 + fast-glob: 3.3.2 + local-pkg: 0.4.3 + magic-string: 0.30.5 + minimatch: 9.0.3 + resolve: 1.22.8 + unplugin: 1.5.0 + vue: 3.3.8(typescript@5.2.2) + transitivePeerDependencies: + - rollup + - supports-color + dev: true + + /unplugin@1.5.0: + resolution: {integrity: sha512-9ZdRwbh/4gcm1JTOkp9lAkIDrtOyOxgHmY7cjuwI8L/2RTikMcVG25GsZwNAgRuap3iDw2jeq7eoqtAsz5rW3A==} + dependencies: + acorn: 8.11.2 + chokidar: 3.5.3 + webpack-sources: 3.2.3 + webpack-virtual-modules: 0.5.0 + dev: true + + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.1 + dev: true + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + /utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + dev: false + + /vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + dev: false + + /vite-plugin-compression@0.5.1(vite@4.5.0): + resolution: {integrity: sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==} + peerDependencies: + vite: '>=2.0.0' + dependencies: + chalk: 4.1.2 + debug: 4.3.4 + fs-extra: 10.1.0 + vite: 4.5.0(sass@1.68.0) + transitivePeerDependencies: + - supports-color + dev: true + + /vite@4.5.0(sass@1.68.0): + resolution: {integrity: sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + esbuild: 0.18.20 + postcss: 8.4.31 + rollup: 3.29.4 + sass: 1.68.0 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /vue-demi@0.14.6(vue@3.3.8): + resolution: {integrity: sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + dependencies: + vue: 3.3.8(typescript@5.2.2) + dev: true + + /vue-eslint-parser@9.3.2(eslint@8.50.0): + resolution: {integrity: sha512-q7tWyCVaV9f8iQyIA5Mkj/S6AoJ9KBN8IeUSf3XEmBrOtxOZnfTg5s4KClbZBCK3GtnT/+RyCLZyDHuZwTuBjg==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + dependencies: + debug: 4.3.4 + eslint: 8.50.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.5.0 + lodash: 4.17.21 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + dev: true + + /vue-i18n@9.5.0(vue@3.3.8): + resolution: {integrity: sha512-NiI3Ph1qMstNf7uhYh8trQBOBFLxeJgcOxBq51pCcZ28Vs18Y7BDS58r8HGDKCYgXdLUYqPDXdKatIF4bvBVZg==} + engines: {node: '>= 16'} + peerDependencies: + vue: ^3.0.0 + dependencies: + '@intlify/core-base': 9.5.0 + '@intlify/shared': 9.5.0 + '@vue/devtools-api': 6.5.1 + vue: 3.3.8(typescript@5.2.2) + dev: true + + /vue-prism-editor@2.0.0-alpha.2(vue@3.3.8): + resolution: {integrity: sha512-Gu42ba9nosrE+gJpnAEuEkDMqG9zSUysIR8SdXUw8MQKDjBnnNR9lHC18uOr/ICz7yrA/5c7jHJr9lpElODC7w==} + engines: {node: '>=10'} + peerDependencies: + vue: ^3.0.0 + dependencies: + vue: 3.3.8(typescript@5.2.2) + dev: true + + /vue-qrcode@2.2.0(qrcode@1.5.3)(vue@3.3.8): + resolution: {integrity: sha512-pEwy/IznxEY5MXptFLaxbGdeDWIJRgU5VhBcFmg1avDjD2z2jjWAGE5dlDwqagXtUjcgkvFSSQ40boog1maLuw==} + peerDependencies: + qrcode: ^1.5.0 + vue: ^2.7.0 || ^3.0.0 + dependencies: + qrcode: 1.5.3 + tslib: 2.6.2 + vue: 3.3.8(typescript@5.2.2) + dev: true + + /vue-router@4.2.5(vue@3.3.8): + resolution: {integrity: sha512-DIUpKcyg4+PTQKfFPX88UWhlagBEBEfJ5A8XDXRJLUnZOvcpMF8o/dnL90vpVkGaPbjvXazV/rC1qBKrZlFugw==} + peerDependencies: + vue: ^3.2.0 + dependencies: + '@vue/devtools-api': 6.5.1 + vue: 3.3.8(typescript@5.2.2) + dev: true + + /vue-toastification@2.0.0-rc.5(vue@3.3.8): + resolution: {integrity: sha512-q73e5jy6gucEO/U+P48hqX+/qyXDozAGmaGgLFm5tXX4wJBcVsnGp4e/iJqlm9xzHETYOilUuwOUje2Qg1JdwA==} + peerDependencies: + vue: ^3.0.2 + dependencies: + vue: 3.3.8(typescript@5.2.2) + dev: true + + /vue@3.3.8(typescript@5.2.2): + resolution: {integrity: sha512-5VSX/3DabBikOXMsxzlW8JyfeLKlG9mzqnWgLQLty88vdZL7ZJgrdgBOmrArwxiLtmS+lNNpPcBYqrhE6TQW5w==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@vue/compiler-dom': 3.3.8 + '@vue/compiler-sfc': 3.3.8 + '@vue/runtime-dom': 3.3.8 + '@vue/server-renderer': 3.3.8(vue@3.3.8) + '@vue/shared': 3.3.8 + typescript: 5.2.2 + dev: true + + /webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: false + + /webpack-sources@3.2.3: + resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} + engines: {node: '>=10.13.0'} + dev: true + + /webpack-virtual-modules@0.5.0: + resolution: {integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==} + dev: true + + /whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: false + + /which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + dev: true + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + + /wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + dependencies: + string-width: 4.2.3 + dev: false + + /wordwrapjs@4.0.1: + resolution: {integrity: sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==} + engines: {node: '>=8.0.0'} + dependencies: + reduce-flatten: 2.0.0 + typical: 5.2.0 + dev: false + + /wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: false + + /wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + dev: false + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + requiresBuild: true + + /ws@8.11.0: + resolution: {integrity: sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + + /xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + dev: true + + /xmlhttprequest-ssl@2.0.0: + resolution: {integrity: sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==} + engines: {node: '>=0.4.0'} + dev: false + + /xterm-addon-web-links@0.9.0(xterm@5.4.0-beta.37): + resolution: {integrity: sha512-LIzi4jBbPlrKMZF3ihoyqayWyTXAwGfu4yprz1aK2p71e9UKXN6RRzVONR0L+Zd+Ik5tPVI9bwp9e8fDTQh49Q==} + peerDependencies: + xterm: ^5.0.0 + dependencies: + xterm: 5.4.0-beta.37 + dev: true + + /xterm@5.4.0-beta.37: + resolution: {integrity: sha512-ys+mXqLFrJc7khmYN/MgBnfLv38NgXfkwkEXsCZKHGqn3h2xUBvTvsrSEWO3NQeDPLj4zMr1RwqTblMK9St3BA==} + dev: true + + /y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + dev: true + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + /yaml@2.3.4: + resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==} + engines: {node: '>= 14'} + dev: false + + /yamljs@0.3.0: + resolution: {integrity: sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==} + hasBin: true + requiresBuild: true + dependencies: + argparse: 1.0.10 + glob: 7.2.3 + dev: false + + /yargs-parser@13.1.2: + resolution: {integrity: sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==} + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + dev: false + + /yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + dev: true + + /yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + dev: true + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + dev: true diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6eff920 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "module": "ESNext", + "target": "ESNext", + "strict": true, + "moduleResolution": "bundler" + } +}